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