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