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