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