]> git.gir.st - VimFx.git/blob - extension/lib/vimfx.coffee
Major refactor: Rework all UI and related improvements
[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, mode) ->
60 return unless keyStr = @stringifyKeyEvent(event)
61
62 now = Date.now()
63 @reset(mode) if now - @lastInputTime >= @options.timeout
64 @lastInputTime = now
65
66 toplevel = (@currentKeyTree == @keyTrees[mode])
67
68 if toplevel and @options.keyValidator
69 unless @options.keyValidator(keyStr, mode)
70 @reset(mode)
71 return
72
73 type = 'none'
74 command = null
75
76 switch
77 when keyStr of @currentKeyTree
78 next = @currentKeyTree[keyStr]
79 if next instanceof Leaf
80 type = 'full'
81 command = next.command
82 else
83 @currentKeyTree = next
84 type = 'partial'
85
86 when toplevel and DIGIT.test(keyStr) and
87 not (keyStr == '0' and @count == '')
88 @count += keyStr
89 type = 'count'
90
91 else
92 @reset(mode)
93
94 count = if @count == '' then undefined else Number(@count)
95 force = if command then (command.pref of @forceCommands) else false
96 @reset(mode) if type == 'full'
97 return {type, command, count, force, keyStr}
98
99 getGroupedCommands: (options = {}) ->
100 modes = {}
101 for modeName, mode of @modes
102 if options.enabledOnly
103 usedSequences = getUsedSequences(@keyTrees[modeName])
104 for commandName, command of mode.commands
105 enabledSequences = null
106 if options.enabledOnly
107 enabledSequences = utils.removeDuplicates(
108 command._sequences.filter((sequence) ->
109 return (usedSequences[sequence] == command.pref)
110 )
111 )
112 continue if enabledSequences.length == 0
113 categories = modes[modeName] ?= {}
114 category = categories[command.category] ?= []
115 category.push({command, enabledSequences, order: command.order})
116
117 modesSorted = []
118 for modeName, categories of modes
119 categoriesSorted = []
120 for categoryName, commands of categories
121 category = @options.categories[categoryName]
122 categoriesSorted.push({
123 name: category.name()
124 _name: categoryName
125 order: category.order
126 commands: commands.sort(byOrder)
127 })
128 mode = @modes[modeName]
129 modesSorted.push({
130 name: mode.name()
131 _name: modeName
132 order: mode.order
133 categories: categoriesSorted.sort(byOrder)
134 })
135 return modesSorted.sort(byOrder)
136
137 byOrder = (a, b) -> a.order - b.order
138
139 class Leaf
140 constructor: (@command, @originalSequence) ->
141
142 createKeyTrees = (modes) ->
143 keyTrees = {}
144 errors = {}
145 forceCommands = {}
146
147 pushError = (error, command) ->
148 (errors[command.pref] ?= []).push(error)
149
150 pushOverrideErrors = (command, tree) ->
151 for { command: overriddenCommand, originalSequence } in getLeaves(tree)
152 error =
153 id: 'overridden_by'
154 subject: command.description()
155 context: originalSequence
156 pushError(error, overriddenCommand)
157 return
158
159 pushForceKeyError = (command, originalSequence) ->
160 error =
161 id: 'illegal_force_key'
162 subject: FORCE_KEY
163 context: originalSequence
164 pushError(error, command)
165
166 for modeName, { commands } of modes
167 keyTrees[modeName] = {}
168 for commandName, command of commands
169 { shortcuts, errors: parseErrors } = parseShortcutPref(command.pref)
170 pushError(error, command) for error in parseErrors
171 command._sequences = []
172
173 for shortcut in shortcuts
174 [ prefixKeys..., lastKey ] = shortcut.normalized
175 tree = keyTrees[modeName]
176 command._sequences.push(shortcut.original)
177
178 for prefixKey, index in prefixKeys
179 if prefixKey == FORCE_KEY
180 if index == 0
181 forceCommands[command.pref] = true
182 else
183 pushForceKeyError(command, shortcut.original)
184 continue
185
186 if prefixKey of tree
187 next = tree[prefixKey]
188 if next instanceof Leaf
189 pushOverrideErrors(command, next)
190 tree = tree[prefixKey] = {}
191 else
192 tree = next
193 else
194 tree = tree[prefixKey] = {}
195
196 if lastKey == FORCE_KEY
197 pushForceKeyError(command, shortcut.original)
198 continue
199 if lastKey of tree
200 pushOverrideErrors(command, tree[lastKey])
201 tree[lastKey] = new Leaf(command, shortcut.original)
202
203 return {keyTrees, forceCommands, errors}
204
205 parseShortcutPref = (pref) ->
206 shortcuts = []
207 errors = []
208
209 # The shorcut prefs are read from root in order to support other extensions to
210 # extend VimFx with custom commands.
211 prefValue = prefs.root.get(pref).trim()
212
213 unless prefValue == ''
214 for sequence in prefValue.split(/\s+/)
215 shortcut = []
216 errored = false
217 for key in notation.parseSequence(sequence)
218 try
219 shortcut.push(notation.normalize(key))
220 catch error
221 throw error unless error.id?
222 errors.push(error)
223 errored = true
224 break
225 shortcuts.push({normalized: shortcut, original: sequence}) unless errored
226
227 return {shortcuts, errors}
228
229 getLeaves = (node) ->
230 if node instanceof Leaf
231 return [node]
232 leaves = []
233 for key, value of node
234 leaves.push(getLeaves(value)...)
235 return leaves
236
237 getUsedSequences = (tree) ->
238 usedSequences = {}
239 for leaf in getLeaves(tree)
240 usedSequences[leaf.originalSequence] = leaf.command.pref
241 return usedSequences
242
243 module.exports = VimFx
Imprint / Impressum