]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Let ctrl and alt in Hints mode control where to open links
[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 notation = require('vim-like-key-notation')
21 prefs = require('./prefs')
22 utils = require('./utils')
23 Vim = require('./vim')
24
25 DIGIT = /^\d$/
26 FORCE_KEY = '<force>'
27
28 class VimFx extends utils.EventEmitter
29 constructor: (@modes, @options) ->
30 super()
31 @vimBucket = new utils.Bucket(((window) => new Vim(window, this)), this)
32 @createKeyTrees()
33 @reset()
34 @on('modechange', ({mode}) => @reset(mode))
35 @currentVim = null
36 @on('bucket.get', (vim) =>
37 return if @currentVim == vim
38 @currentVim = vim
39 @emit('currentVimChange', vim)
40 )
41 @customCommandCounter = 0
42
43 reset: (mode = null) ->
44 @currentKeyTree = if mode then @keyTrees[mode] else {}
45 @lastInputTime = 0
46 @count = ''
47
48 getCurrentLocation: -> @currentVim?.window.location
49
50 createKeyTrees: ->
51 {@keyTrees, @forceCommands, @errors} = createKeyTrees(@modes)
52
53 stringifyKeyEvent: (event) ->
54 return notation.stringify(event, {
55 ignoreKeyboardLayout: @options.ignore_keyboard_layout
56 translations: @options.translations
57 })
58
59 consumeKeyEvent: (event, vim) ->
60 { mode } = vim
61 return unless keyStr = @stringifyKeyEvent(event)
62
63 now = Date.now()
64 @reset(mode) if now - @lastInputTime >= @options.timeout
65 @lastInputTime = now
66
67 toplevel = (@currentKeyTree == @keyTrees[mode])
68
69 if toplevel and @options.keyValidator
70 unless @options.keyValidator(keyStr, mode)
71 @reset(mode)
72 return
73
74 type = 'none'
75 command = null
76
77 switch
78 when keyStr of @currentKeyTree
79 next = @currentKeyTree[keyStr]
80 if next instanceof Leaf
81 type = 'full'
82 command = next.command
83 else
84 @currentKeyTree = next
85 type = 'partial'
86
87 when toplevel and DIGIT.test(keyStr) and
88 not (keyStr == '0' and @count == '')
89 @count += keyStr
90 type = 'count'
91
92 else
93 @reset(mode)
94
95 count = if @count == '' then undefined else Number(@count)
96 force = if command then (command.pref of @forceCommands) else false
97 focus = @getFocusType(vim, event, keyStr)
98 unmodifiedKey = notation.parse(keyStr).key
99 @reset(mode) if type == 'full'
100 return {type, focus, command, count, force, keyStr, unmodifiedKey, toplevel}
101
102 getFocusType: (vim, event, keyStr) ->
103 return unless target = event.originalTarget # For the tests.
104 document = target.ownerDocument
105
106 { activatable_element_keys, adjustable_element_keys } = @options
107 return switch
108 when utils.isTextInputElement(target) or
109 utils.isContentEditable(target)
110 'editable'
111 when (utils.isActivatable(target) and
112 keyStr in activatable_element_keys)
113 'activatable'
114 when (utils.isAdjustable(target) and
115 keyStr in adjustable_element_keys)
116 'adjustable'
117 when vim.rootWindow.TabView.isVisible() or
118 document.fullscreenElement or document.mozFullScreenElement
119 'other'
120 else
121 null
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 = (modes) ->
167 keyTrees = {}
168 errors = {}
169 forceCommands = {}
170
171 pushError = (error, command) ->
172 (errors[command.pref] ?= []).push(error)
173
174 pushOverrideErrors = (command, tree) ->
175 for { command: overriddenCommand, originalSequence } in getLeaves(tree)
176 error =
177 id: 'overridden_by'
178 subject: command.description()
179 context: originalSequence
180 pushError(error, overriddenCommand)
181 return
182
183 pushForceKeyError = (command, originalSequence) ->
184 error =
185 id: 'illegal_force_key'
186 subject: FORCE_KEY
187 context: originalSequence
188 pushError(error, command)
189
190 for modeName, { commands } of modes
191 keyTrees[modeName] = {}
192 for commandName, command of commands
193 { shortcuts, errors: parseErrors } = parseShortcutPref(command.pref)
194 pushError(error, command) for error in parseErrors
195 command._sequences = []
196
197 for shortcut in shortcuts
198 [ prefixKeys..., lastKey ] = shortcut.normalized
199 tree = keyTrees[modeName]
200 command._sequences.push(shortcut.original)
201
202 for prefixKey, index in prefixKeys
203 if prefixKey == FORCE_KEY
204 if index == 0
205 forceCommands[command.pref] = true
206 else
207 pushForceKeyError(command, shortcut.original)
208 continue
209
210 if prefixKey of tree
211 next = tree[prefixKey]
212 if next instanceof Leaf
213 pushOverrideErrors(command, next)
214 tree = tree[prefixKey] = {}
215 else
216 tree = next
217 else
218 tree = tree[prefixKey] = {}
219
220 if lastKey == FORCE_KEY
221 pushForceKeyError(command, shortcut.original)
222 continue
223 if lastKey of tree
224 pushOverrideErrors(command, tree[lastKey])
225 tree[lastKey] = new Leaf(command, shortcut.original)
226
227 return {keyTrees, forceCommands, errors}
228
229 parseShortcutPref = (pref) ->
230 shortcuts = []
231 errors = []
232
233 # The shorcut prefs are read from root in order to support other extensions to
234 # extend VimFx with custom commands.
235 prefValue = prefs.root.get(pref).trim()
236
237 unless prefValue == ''
238 for sequence in prefValue.split(/\s+/)
239 shortcut = []
240 errored = false
241 for key in notation.parseSequence(sequence)
242 try
243 shortcut.push(notation.normalize(key))
244 catch error
245 throw error unless error.id?
246 errors.push(error)
247 errored = true
248 break
249 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
250
251 return {shortcuts, errors}
252
253 getLeaves = (node) ->
254 if node instanceof Leaf
255 return [node]
256 leaves = []
257 for key, value of node
258 leaves.push(getLeaves(value)...)
259 return leaves
260
261 getUsedSequences = (tree) ->
262 usedSequences = {}
263 for leaf in getLeaves(tree)
264 usedSequences[leaf.originalSequence] = leaf.command.pref
265 return usedSequences
266
267 module.exports = VimFx
Imprint / Impressum