]> git.gir.st - VimFx.git/blob - extension/lib/api.coffee
Change license to MIT
[VimFx.git] / extension / lib / api.coffee
1 # This file defines VimFx’s config file API.
2
3 defaults = require('./defaults')
4 prefs = require('./prefs')
5 utils = require('./utils')
6 Vim = require('./vim')
7
8 counter = new utils.Counter({start: 10000, step: 100})
9
10 createConfigAPI = (vimfx, {allowDeprecated = true} = {}) -> {
11 get: (inputPref) ->
12 pref = alias(inputPref, allowDeprecated)
13 if pref != inputPref
14 try return prefs.get(inputPref)
15 return switch
16 when pref of defaults.parsed_options
17 vimfx.options[pref]
18 when pref of defaults.all_prefs or pref?.startsWith('custom.')
19 prefs.get(pref)
20 else
21 throw new Error("VimFx: Unknown option: #{pref}")
22
23 getDefault: (inputPref) ->
24 pref = alias(inputPref, allowDeprecated)
25 if pref != inputPref
26 try return prefs.default.get(inputPref)
27 return switch
28 when pref of defaults.parsed_options or pref?.startsWith('custom.')
29 throw new Error("VimFx: No default for option: #{pref}")
30 when pref of defaults.all_prefs
31 defaults.all_prefs[pref]
32 else
33 throw new Error("VimFx: Unknown option: #{pref}")
34
35 set: (inputPref, value) ->
36 pref = alias(inputPref, allowDeprecated)
37 switch
38 when pref of defaults.parsed_options
39 previousValue = vimfx.options[pref]
40 vimfx.options[pref] = value
41 onShutdown(vimfx, -> vimfx.options[pref] = previousValue)
42 when pref of defaults.all_prefs or pref?.startsWith('custom.')
43 previousValue = if prefs.has(pref) then prefs.get(pref) else null
44 prefs.set(pref, value)
45 onShutdown(vimfx, -> prefs.set(pref, previousValue))
46 else
47 throw new Error("VimFx: Unknown option: #{pref}")
48
49 addCommand: ({name, description, mode, category, order} = {}, fn) ->
50 mode ?= 'normal'
51 category ?= if mode == 'normal' then 'misc' else ''
52 order ?= counter.tick()
53
54 unless typeof name == 'string'
55 throw new Error(
56 "VimFx: A command name as a string is required. Got: #{name}"
57 )
58 unless /^[a-z_]+$/.test(name)
59 throw new Error(
60 "VimFx: Command names should only consist of a-z (lowercase) and
61 underscores. Got: #{name}"
62 )
63 unless typeof description == 'string' and description != ''
64 throw new Error(
65 "VimFx: Commands must have a non-empty description. Got: #{description}"
66 )
67 unless utils.has(vimfx.modes, mode)
68 modes = Object.keys(vimfx.modes).join(', ')
69 throw new Error(
70 "VimFx: Unknown mode. Available modes are: #{modes}. Got: #{mode}"
71 )
72 unless utils.has(vimfx.options.categories, category)
73 categories = Object.keys(vimfx.options.categories).join(', ')
74 throw new Error(
75 "VimFx: Unknown category. Available categories are: #{categories}.
76 Got: #{category}"
77 )
78 unless typeof order == 'number'
79 throw new Error("VimFx: Command order must be a number. Got: #{order}")
80 unless typeof fn == 'function'
81 throw new Error("VimFx: Commands need a function to run. Got: #{fn}")
82
83 pref = "#{defaults.BRANCH}custom.mode.#{mode}.#{name}"
84 prefs.root.default.set(pref, '')
85 vimfx.modes[mode].commands[name] = {
86 pref, category, order, run: fn, description
87 }
88 onShutdown(vimfx, -> delete vimfx.modes[mode].commands[name])
89
90 addOptionOverrides: (rules...) ->
91 validateRules(rules, (override) ->
92 unless Object::toString.call(override) == '[object Object]'
93 return 'an object'
94 return null
95 )
96
97 unless vimfx.optionOverrides
98 vimfx.optionOverrides = []
99 vimfx.options = new Proxy(vimfx.options, {
100 get: (options, pref) ->
101 location = utils.getCurrentLocation()
102 return options[pref] unless location
103 overrides = getOverrides(vimfx.optionOverrides ? [], location)
104 return overrides?[pref] ? options[pref]
105 })
106 onShutdown(vimfx, -> vimfx.optionOverrides = null)
107
108 vimfx.optionOverrides.push(rules...)
109
110 addKeyOverrides: (rules...) ->
111 validateRules(rules, (override) ->
112 unless Array.isArray(override) and
113 override.every((item) -> typeof item == 'string')
114 return 'an array of strings'
115 return null
116 )
117
118 unless vimfx.keyOverrides
119 vimfx.keyOverrides = []
120 vimfx.options.keyValidator = (keyStr, mode) ->
121 return true unless mode == 'normal'
122 location = utils.getCurrentLocation()
123 return true unless location
124 overrides = getOverrides(vimfx.keyOverrides ? [], location)
125 return keyStr not in (overrides ? [])
126 onShutdown(vimfx, -> vimfx.keyOverrides = null)
127
128 vimfx.keyOverrides.push(rules...)
129
130 send: (vim, message, data = null, callback = null) ->
131 unless vim instanceof Vim
132 throw new Error(
133 "VimFx: The first argument must be a vim object. Got: #{vim}"
134 )
135 unless typeof message == 'string'
136 throw new Error(
137 "VimFx: The second argument must be a message string. Got: #{message}"
138 )
139 if typeof data == 'function'
140 throw new Error(
141 "VimFx: The third argument must not be a function. Got: #{data}"
142 )
143
144 unless typeof callback == 'function' or callback == null
145 throw Error(
146 "VimFx: If provided, `callback` must be a function. Got: #{callback}"
147 )
148 vim._send(message, data, callback, {prefix: 'config:'})
149
150 on: (event, listener) ->
151 validateEventListener(event, listener)
152 vimfx.on(event, listener)
153 onShutdown(vimfx, -> vimfx.off(event, listener))
154
155 off: (event, listener) ->
156 validateEventListener(event, listener)
157 vimfx.off(event, listener)
158
159 modes: vimfx.modes
160 }
161
162 # Don’t crash the users’s entire config file on startup if they happen to try to
163 # set a renamed pref (only warn), but do throw an error if they reload the
164 # config file; then they could update while editing the file anyway.
165 alias = (pref, allowDeprecated) ->
166 if pref of renamedPrefs
167 newPref = renamedPrefs[pref]
168 message = "VimFx: `#{pref}` has been renamed to `#{newPref}`."
169 if allowDeprecated
170 console.warn(message)
171 return newPref
172 else
173 throw new Error(message)
174 else
175 return pref
176
177 renamedPrefs = {
178 'hint_chars': 'hints.chars'
179 'hints_sleep': 'hints.sleep'
180 'hints_timeout': 'hints.matched_timeout'
181 'hints_peek_through': 'hints.peek_through'
182 'hints_toggle_in_tab': 'hints.toggle_in_tab'
183 'hints_toggle_in_background': 'hints.toggle_in_background'
184 'mode.hints.delete_hint_char': 'mode.hints.delete_char'
185 }
186
187 getOverrides = (rules, args...) ->
188 for [matcher, override] in rules
189 return override if matcher(args...)
190 return null
191
192 validateRules = (rules, overrideValidator) ->
193 for rule in rules
194 unless Array.isArray(rule)
195 throw new Error(
196 "VimFx: An override rule must be an array. Got: #{rule}"
197 )
198 unless rule.length == 2
199 throw new Error(
200 "VimFx: An override rule array must be of length 2. Got: #{rule.length}"
201 )
202 [matcher, override] = rule
203 unless typeof matcher == 'function'
204 throw new Error(
205 "VimFx: The first item of an override rule array must be a function.
206 Got: #{matcher}"
207 )
208 overrideValidationMessage = overrideValidator(override)
209 if overrideValidationMessage
210 throw new Error(
211 "VimFx: The second item of an override rule array must be
212 #{overrideValidationMessage}. Got: #{override}"
213 )
214 return
215
216 validateEventListener = (event, listener) ->
217 unless typeof event == 'string'
218 throw new Error(
219 "VimFx: The first argument must be a string. Got: #{event}"
220 )
221 unless typeof listener == 'function'
222 throw new Error(
223 "VimFx: The second argument must be a function. Got: #{listener}"
224 )
225 return
226
227 onShutdown = (vimfx, handler) ->
228 fn = ->
229 handler()
230 vimfx.off('shutdown', fn)
231 vimfx.on('shutdown', fn)
232
233 module.exports = createConfigAPI
Imprint / Impressum