]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Improve keyboard shortcut overriding behavior
[VimFx.git] / extension / lib / vimfx.coffee
1 ###
2 # Copyright Simon Lydell 2015.
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 FORCE_KEY = '<force>'
32
33 class VimFx extends utils.EventEmitter
34 constructor: (@modes, @options) ->
35 super()
36 @vims = new WeakMap()
37 @createKeyTrees()
38 @reset()
39 @on('modechange', ({mode}) => @reset(mode))
40
41 addVim: (browser) ->
42 @vims.set(browser, new Vim(browser, this))
43
44 getCurrentVim: (window) -> @vims.get(window.gBrowser.selectedBrowser)
45
46 reset: (mode = null) ->
47 @currentKeyTree = if mode then @keyTrees[mode] else {}
48 @lastInputTime = 0
49 @count = ''
50
51 createKeyTrees: ->
52 {@keyTrees, @forceCommands, @errors} = createKeyTrees(@getGroupedCommands())
53
54 stringifyKeyEvent: (event) ->
55 return notation.stringify(event, {
56 ignoreKeyboardLayout: @options.ignore_keyboard_layout
57 translations: @options.translations
58 })
59
60 consumeKeyEvent: (event, vim, focusType) ->
61 { mode } = vim
62 return unless keyStr = @stringifyKeyEvent(event)
63
64 now = Date.now()
65 @reset(mode) if now - @lastInputTime >= @options.timeout
66 @lastInputTime = now
67
68 toplevel = (@currentKeyTree == @keyTrees[mode])
69
70 if toplevel and @options.keyValidator
71 unless @options.keyValidator(keyStr, mode)
72 @reset(mode)
73 return
74
75 type = 'none'
76 command = null
77
78 switch
79 when toplevel and DIGIT.test(keyStr) and
80 not (keyStr == '0' and @count == '')
81 @count += keyStr
82 type = 'count'
83
84 when keyStr of @currentKeyTree
85 next = @currentKeyTree[keyStr]
86 if next instanceof Leaf
87 type = 'full'
88 command = next.command
89 else
90 @currentKeyTree = next
91 type = 'partial'
92
93 else
94 @reset(mode)
95
96 count = if @count == '' then undefined else Number(@count)
97 force = if command then (command.pref of @forceCommands) else false
98 focus = @adjustFocusType(event, vim, focusType, keyStr)
99 unmodifiedKey = notation.parse(keyStr).key
100 @reset(mode) if type == 'full'
101 return {type, focus, command, count, force, keyStr, unmodifiedKey, toplevel}
102
103 adjustFocusType: (event, vim, focusType, keyStr) ->
104 # Frame scripts and the tests don’t pass in `originalTarget`.
105 document = event.originalTarget?.ownerDocument
106 if focusType == null and document and
107 (vim.window.TabView.isVisible() or
108 document.fullscreenElement or document.mozFullScreenElement)
109 return 'other'
110
111 keys = @options["#{ focusType }_element_keys"]
112 return null if keys and keyStr not in keys
113
114 return focusType
115
116 getGroupedCommands: (options = {}) ->
117 modes = {}
118 for modeName, mode of @modes
119 if options.enabledOnly
120 usedSequences = getUsedSequences(@keyTrees[modeName])
121 for commandName, command of mode.commands
122 enabledSequences = null
123 if options.enabledOnly
124 enabledSequences = utils.removeDuplicates(
125 command._sequences.filter((sequence) ->
126 return (usedSequences[sequence] == command.pref)
127 )
128 )
129 continue if enabledSequences.length == 0
130 categories = modes[modeName] ?= {}
131 category = categories[command.category] ?= []
132 category.push({command, enabledSequences, order: command.order})
133
134 modesSorted = []
135 for modeName, categories of modes
136 categoriesSorted = []
137 for categoryName, commands of categories
138 category = @options.categories[categoryName]
139 categoriesSorted.push({
140 name: category.name()
141 _name: categoryName
142 order: category.order
143 commands: commands.sort(byOrder)
144 })
145 mode = @modes[modeName]
146 modesSorted.push({
147 name: mode.name()
148 _name: modeName
149 order: mode.order
150 categories: categoriesSorted.sort(byOrder)
151 })
152 return modesSorted.sort(byOrder)
153
154 byOrder = (a, b) -> a.order - b.order
155
156 class Leaf
157 constructor: (@command, @originalSequence) ->
158
159 createKeyTrees = (groupedCommands) ->
160 keyTrees = {}
161 errors = {}
162 forceCommands = {}
163
164 pushError = (error, command) ->
165 (errors[command.pref] ?= []).push(error)
166
167 pushOverrideErrors = (command, tree) ->
168 { command: overridingCommand, originalSequence } = getFirstLeaf(tree)
169 error =
170 id: 'overridden_by'
171 subject: overridingCommand.description()
172 context: originalSequence
173 pushError(error, command)
174
175 pushForceKeyError = (command, originalSequence) ->
176 error =
177 id: 'illegal_force_key'
178 subject: FORCE_KEY
179 context: originalSequence
180 pushError(error, command)
181
182 for mode in groupedCommands
183 keyTrees[mode._name] = {}
184 for category in mode.categories then for { command } in category.commands
185 { shortcuts, errors: parseErrors } = parseShortcutPref(command.pref)
186 pushError(error, command) for error in parseErrors
187 command._sequences = []
188
189 for shortcut in shortcuts
190 [ prefixKeys..., lastKey ] = shortcut.normalized
191 tree = keyTrees[mode._name]
192 command._sequences.push(shortcut.original)
193
194 errored = false
195 for prefixKey, index in prefixKeys
196 if prefixKey == FORCE_KEY
197 if index == 0
198 forceCommands[command.pref] = true
199 continue
200 else
201 pushForceKeyError(command, shortcut.original)
202 errored = true
203 break
204
205 if prefixKey of tree
206 next = tree[prefixKey]
207 if next instanceof Leaf
208 pushOverrideErrors(command, next)
209 errored = true
210 break
211 else
212 tree = next
213 else
214 tree = tree[prefixKey] = {}
215 continue if errored
216
217 if lastKey == FORCE_KEY
218 pushForceKeyError(command, shortcut.original)
219 continue
220 if lastKey of tree
221 pushOverrideErrors(command, tree[lastKey])
222 continue
223 tree[lastKey] = new Leaf(command, shortcut.original)
224
225 return {keyTrees, forceCommands, errors}
226
227 parseShortcutPref = (pref) ->
228 shortcuts = []
229 errors = []
230
231 # The shorcut prefs are read from root in order to support other extensions to
232 # extend VimFx with custom commands.
233 prefValue = prefs.root.get(pref).trim()
234
235 unless prefValue == ''
236 for sequence in prefValue.split(/\s+/)
237 shortcut = []
238 errored = false
239 for key in notation.parseSequence(sequence)
240 try
241 shortcut.push(notation.normalize(key))
242 catch error
243 throw error unless error.id?
244 errors.push(error)
245 errored = true
246 break
247 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
248
249 return {shortcuts, errors}
250
251 getFirstLeaf = (node) ->
252 if node instanceof Leaf
253 return node
254 for key, value of node
255 return getFirstLeaf(value)
256
257 getLeaves = (node) ->
258 if node instanceof Leaf
259 return [node]
260 leaves = []
261 for key, value of node
262 leaves.push(getLeaves(value)...)
263 return leaves
264
265 getUsedSequences = (tree) ->
266 usedSequences = {}
267 for leaf in getLeaves(tree)
268 usedSequences[leaf.originalSequence] = leaf.command.pref
269 return usedSequences
270
271 module.exports = VimFx
Imprint / Impressum