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