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