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