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