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