]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Make Caret browsing handling more robust
[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 keypresses 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 CARET_BROWSING_PREF = 'accessibility.browsewithcaret'
31 DIGIT = /^\d$/
32
33 class VimFx extends utils.EventEmitter
34 constructor: (@modes, @options) ->
35 super()
36 @vims = new WeakMap()
37 @lastClosedVim = null
38 @goToCommand = null
39 @skipObserveCaretBrowsing = false
40 @ignoreKeyEventsUntilTime = 0
41 @skipCreateKeyTrees = false
42 @createKeyTrees()
43 @reset()
44 @observeCaretBrowsing()
45 @on('modeChange', ({vim}) => @reset(vim.mode))
46
47 SPECIAL_KEYS: {
48 '<force>': {}
49 '<late>': {single: true}
50 }
51
52 MODIFIER_KEYS: {
53 '<alt>': true
54 '<control>': true
55 '<ctrl>': true
56 '<meta>': true
57 '<shift>': true
58 }
59
60 addVim: (browser) ->
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()`.
67 vim._start()
68
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
72 # returned, such as:
73 #
74 # return unless vim = @vimfx.getCurrentVim(@window)
75 getCurrentVim: (window) -> @vims.get(window.gBrowser.selectedBrowser)
76
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] ? {}
81 @lastInputTime = 0
82 @count = ''
83
84 resetCaretBrowsing: (value = @options.browsewithcaret) ->
85 @skipObserveCaretBrowsing = true
86 prefs.root.set(CARET_BROWSING_PREF, value)
87 @skipObserveCaretBrowsing = false
88
89 observeCaretBrowsing: ->
90 prefs.root.observe(CARET_BROWSING_PREF, =>
91 return if @skipObserveCaretBrowsing
92 prefs.set('browsewithcaret', prefs.root.get(CARET_BROWSING_PREF))
93 )
94
95 createKeyTrees: ->
96 return if @skipCreateKeyTrees
97 {@keyTrees, @errors} =
98 createKeyTrees(@getGroupedCommands(), @SPECIAL_KEYS, @MODIFIER_KEYS)
99
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
106 })
107
108 consumeKeyEvent: (event, vim) ->
109 {mode} = vim
110 return unless keyStr = @stringifyKeyEvent(event)
111
112 now = Date.now()
113
114 return true if now <= @ignoreKeyEventsUntilTime
115
116 @reset(mode) if now - @lastInputTime >= @options.timeout
117 @lastInputTime = now
118
119 toplevel = (@currentKeyTree == @keyTrees[mode])
120
121 if toplevel and @options.keyValidator
122 unless @options.keyValidator(keyStr, mode)
123 @reset(mode)
124 return
125
126 type = 'none'
127 command = null
128 specialKeys = {}
129
130 switch
131 when keyStr of @currentKeyTree and
132 not (toplevel and keyStr == '0' and @count != '')
133 next = @currentKeyTree[keyStr]
134 if next instanceof Leaf
135 type = 'full'
136 {command, specialKeys} = next
137 else
138 @currentKeyTree = next
139 type = 'partial'
140
141 when @options.counts_enabled and toplevel and DIGIT.test(keyStr) and
142 not (keyStr == '0' and @count == '')
143 @count += keyStr
144 type = 'count'
145
146 else
147 @reset(mode)
148
149 count = if @count == '' then undefined else Number(@count)
150 unmodifiedKey = notation.parse(keyStr).key
151
152 focusTypeKeys = @options["#{vim.focusType}_element_keys"]
153 likelyConflict =
154 if toplevel
155 if focusTypeKeys
156 keyStr in focusTypeKeys
157 else
158 vim.focusType != 'none'
159 else
160 false
161
162 @reset(mode) if type == 'full'
163 return {
164 type, command, count, toplevel
165 specialKeys, keyStr, unmodifiedKey, likelyConflict
166 rawKey: event.key, rawCode: event.code
167 discard: @reset.bind(this, mode)
168 }
169
170 getGroupedCommands: (options = {}) ->
171 modes = {}
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)
181 )
182 )
183 continue if enabledSequences.length == 0
184 categories = modes[modeName] ?= {}
185 category = categories[command.category] ?= []
186 category.push(
187 {command, enabledSequences, order: command.order, name: commandName}
188 )
189
190 modesSorted = []
191 for modeName, categories of modes
192 categoriesSorted = []
193 for categoryName, commands of categories
194 category = @options.categories[categoryName]
195 categoriesSorted.push({
196 name: category.name
197 _name: categoryName
198 order: category.order
199 commands: commands.sort(byOrder)
200 })
201 mode = @modes[modeName]
202 modesSorted.push({
203 name: mode.name
204 _name: modeName
205 order: mode.order
206 categories: categoriesSorted.sort(byOrder)
207 })
208 return modesSorted.sort(byOrder)
209
210 byOrder = (a, b) -> a.order - b.order
211
212 class Leaf
213 constructor: (@command, @originalSequence, @specialKeys) ->
214
215 createKeyTrees = (groupedCommands, specialKeysSpec, modifierKeys) ->
216 keyTrees = {}
217 errors = {}
218
219 pushError = (error, command) ->
220 (errors[command.pref] ?= []).push(error)
221
222 pushOverrideErrors = (command, originalSequence, tree) ->
223 {command: overridingCommand} = getFirstLeaf(tree)
224 error = {
225 id: 'overridden_by'
226 subject: overridingCommand.description
227 context: originalSequence
228 }
229 pushError(error, command)
230
231 pushKeyError = (command, id, originalSequence, key) ->
232 pushError({id, subject: key, context: originalSequence}, command)
233
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 = []
240
241 for shortcut in shortcuts
242 [prefixKeys..., lastKey] = shortcut.normalized
243 tree = keyTrees[mode._name]
244 command._sequences.push(shortcut.original)
245 seenNonSpecialKey = false
246 specialKeys = {}
247 errored = false
248
249 for prefixKey, index in prefixKeys
250 if prefixKey of modifierKeys
251 pushKeyError(
252 command, 'mistake_modifier', shortcut.original, prefixKey
253 )
254 errored = true
255 break
256 if prefixKey of specialKeysSpec
257 if seenNonSpecialKey
258 pushKeyError(
259 command, 'special_key.prefix_only', shortcut.original, prefixKey
260 )
261 errored = true
262 break
263 else
264 specialKeys[prefixKey] = true
265 continue
266 unless seenNonSpecialKey
267 for specialKey of specialKeys
268 options = specialKeysSpec[specialKey]
269 if options.single
270 pushKeyError(
271 command, 'special_key.single_only', shortcut.original,
272 specialKey
273 )
274 errored = true
275 break
276 break if errored
277 seenNonSpecialKey = true
278
279 if prefixKey of tree
280 next = tree[prefixKey]
281 if next instanceof Leaf
282 pushOverrideErrors(command, shortcut.original, next)
283 errored = true
284 break
285 else
286 tree = next
287 else
288 tree = tree[prefixKey] = {}
289
290 continue if errored
291
292 if lastKey of modifierKeys
293 pushKeyError(command, 'lone_modifier', shortcut.original, lastKey)
294 errored = true
295 continue
296 if lastKey of specialKeysSpec
297 subject = if seenNonSpecialKey then lastKey else shortcut.original
298 pushKeyError(
299 command, 'special_key.prefix_only', shortcut.original, subject
300 )
301 continue
302 if lastKey of tree
303 pushOverrideErrors(command, shortcut.original, tree[lastKey])
304 continue
305 tree[lastKey] = new Leaf(command, shortcut.original, specialKeys)
306
307 return {keyTrees, errors}
308
309 parseShortcutPref = (pref) ->
310 shortcuts = []
311 errors = []
312
313 prefValue = prefs.root.get(pref).trim()
314
315 unless prefValue == ''
316 for sequence in prefValue.split(/\s+/)
317 shortcut = []
318 errored = false
319 for key in notation.parseSequence(sequence)
320 try
321 shortcut.push(notation.normalize(key))
322 catch error
323 throw error unless error.id?
324 errors.push(error)
325 errored = true
326 break
327 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
328
329 return {shortcuts, errors}
330
331 getFirstLeaf = (node) ->
332 if node instanceof Leaf
333 return node
334 for key, value of node
335 return getFirstLeaf(value)
336
337 getLeaves = (node) ->
338 if node instanceof Leaf
339 return [node]
340 leaves = []
341 for key, value of node
342 leaves.push(getLeaves(value)...)
343 return leaves
344
345 getUsedSequences = (tree) ->
346 usedSequences = {}
347 for leaf in getLeaves(tree)
348 usedSequences[leaf.originalSequence] = leaf.command.pref
349 return usedSequences
350
351 module.exports = VimFx
Imprint / Impressum