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