sublime_dsl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +136 -0
  3. data/Rakefile +248 -0
  4. data/SYNTAX.md +927 -0
  5. data/bin/subdsl +4 -0
  6. data/lib/sublime_dsl/cli/export.rb +134 -0
  7. data/lib/sublime_dsl/cli/import.rb +143 -0
  8. data/lib/sublime_dsl/cli.rb +125 -0
  9. data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
  10. data/lib/sublime_dsl/core_ext/string.rb +129 -0
  11. data/lib/sublime_dsl/core_ext.rb +4 -0
  12. data/lib/sublime_dsl/sublime_text/command.rb +157 -0
  13. data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
  14. data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
  15. data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
  16. data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
  17. data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
  18. data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
  19. data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
  20. data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
  21. data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
  22. data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
  23. data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
  24. data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
  25. data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
  26. data/lib/sublime_dsl/sublime_text/package.rb +96 -0
  27. data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
  28. data/lib/sublime_dsl/sublime_text.rb +48 -0
  29. data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
  30. data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
  31. data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
  32. data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
  33. data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
  34. data/lib/sublime_dsl/textmate/grammar.rb +252 -0
  35. data/lib/sublime_dsl/textmate/plist.rb +141 -0
  36. data/lib/sublime_dsl/textmate/preference.rb +301 -0
  37. data/lib/sublime_dsl/textmate/snippet.rb +437 -0
  38. data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
  39. data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
  40. data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
  41. data/lib/sublime_dsl/textmate/theme.rb +364 -0
  42. data/lib/sublime_dsl/textmate.rb +9 -0
  43. data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
  44. data/lib/sublime_dsl/tools/console.rb +74 -0
  45. data/lib/sublime_dsl/tools/helpers.rb +152 -0
  46. data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
  47. data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
  48. data/lib/sublime_dsl/tools/value_equality.rb +37 -0
  49. data/lib/sublime_dsl/tools/xml.rb +66 -0
  50. data/lib/sublime_dsl/tools.rb +66 -0
  51. data/lib/sublime_dsl.rb +23 -0
  52. metadata +145 -0
@@ -0,0 +1,194 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class KeyMap
6
+
7
+ ##
8
+ # Keymap DSL interpreter.
9
+
10
+ class DSLReader < Tools::BlankSlate
11
+
12
+ attr_accessor :_file
13
+
14
+ def initialize(file = nil)
15
+ @keymaps = []
16
+ @_file = file
17
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
18
+ end
19
+
20
+ def _keymaps
21
+ @keymaps
22
+ end
23
+
24
+ def method_missing(sym, *args, &block)
25
+ "invalid DSL statement: '#{sym}'"
26
+ end
27
+
28
+ def keymap(name, &block)
29
+ reader = BindingReader.new(_file)
30
+ reader.instance_eval(&block)
31
+ # unless reader._catchers_hash.empty?
32
+ # reader._catchers_hash.each_pair { |i,c| p c }
33
+ # end
34
+ map = KeyMap.new(name, reader._keyboard)
35
+ map.bindings.concat reader._bindings
36
+ @keymaps << map
37
+ end
38
+
39
+ end
40
+
41
+ ##
42
+ # Catches all method calls as MethodCatcher objects.
43
+
44
+ class MethodCatcher < Tools::BlankSlate
45
+
46
+ def initialize(object = nil, method = nil, args = nil)
47
+ @object = object
48
+ @method = method
49
+ @args = args
50
+ end
51
+
52
+ # Returns a new MethodCatcher for +self+, +sym+ and +args+.
53
+ def method_missing(sym, *args)
54
+ # puts "creating catcher: "
55
+ # puts " object=#{self.inspect}"
56
+ # puts " sym=#{sym.inspect}"
57
+ # puts " args=#{args.inspect}"
58
+ MethodCatcher.new(self, sym, args)
59
+ end
60
+
61
+ def _object; @object end
62
+ def _method; @method end
63
+ def _args; @args end
64
+
65
+ def is_a?(klass)
66
+ klass == MethodCatcher
67
+ end
68
+
69
+ def inspect
70
+ "<#MethodCatcher object=#{@object.inspect} method=#{@method.inspect} args=#{@args.inspect}>"
71
+ end
72
+
73
+ end
74
+
75
+ ##
76
+ # DSL interpreter for bindings.
77
+
78
+ class BindingReader < MethodCatcher
79
+
80
+ def initialize(file)
81
+ super(nil, nil, nil)
82
+ @file = file
83
+ @bindings = []
84
+ @keyboard = Keyboard.sublime
85
+ @conditionals = nil
86
+ @catchers_hash = {}
87
+ end
88
+
89
+ def _bindings
90
+ @bindings
91
+ end
92
+
93
+ def _keyboard
94
+ @keyboard
95
+ end
96
+
97
+ def _catchers_hash
98
+ @catchers_hash
99
+ end
100
+
101
+ def _conditionals
102
+ @conditionals ||= { if: '_if', and: '_and', or: '_or' }
103
+ end
104
+
105
+ def method_missing(sym, *args)
106
+ catcher = super(sym, *args)
107
+ @catchers_hash[catcher.object_id] = catcher
108
+ catcher
109
+ end
110
+
111
+ def keyboard(name)
112
+ # FIXME: this is dirty
113
+ # assumes the root is the directory above the one containing
114
+ # the current file
115
+ dir = File.dirname(@file)
116
+ @keyboard = Keyboard.get(name, "#{dir}/..")
117
+ end
118
+
119
+ def conditionals(options = {})
120
+ @conditionals = options.dup
121
+ [:if, :and, :or].each do |key|
122
+ method = options.delete(key) or raise Error, "no method name for #{key.inspect}"
123
+ define_singleton_method method.to_sym, self.method("_#{key}".to_sym)
124
+ end
125
+ options.empty? or
126
+ warn "unknown 'conditionals' arguments ignored: #{options.inspect}"
127
+ end
128
+
129
+ def bind(spec, arg, &block)
130
+ ks = spec.split(/,\s+/).map { |s| @keyboard.ensure_keystroke(s) }
131
+ cmd = get_command(arg)
132
+ cmd.error and raise Error, "binding '#{spec}': #{cmd.error}"
133
+ b = KeyBinding.new(ks, cmd)
134
+ @bindings << b
135
+ end
136
+
137
+ def _if(*args, &block)
138
+ b = @bindings.last or
139
+ raise Error, "'#{_conditionals[:if]}' without a previous 'bind'"
140
+ b.add_condition get_condition(args)
141
+ end
142
+
143
+ def _and(*args, &block)
144
+ b = @bindings.last or
145
+ raise Error, "'#{_conditionals[:and]}' without a previous '#{_conditionals[:if]}'"
146
+ b.add_condition get_condition(args)
147
+ end
148
+
149
+ def _or(*args, &block)
150
+ b = @bindings.last or
151
+ raise Error, "'#{_conditionals[:or]}' without a previous '#{_conditionals[:if]}'"
152
+ b = KeyBinding.new(b.keystrokes, b.command)
153
+ @bindings << b
154
+ b.add_condition get_condition(args)
155
+ end
156
+
157
+ private
158
+
159
+ def get_command(arg)
160
+ arg.is_a?(MethodCatcher) or
161
+ return Command.new(nil, nil, "expected a sublime text command: #{arg.inspect}")
162
+ consumed_catcher arg
163
+ Command.from_method_missing(arg._method, arg._args)
164
+ end
165
+
166
+ def get_condition(args)
167
+ args.map { |e| flatten_catchers(e) }.flatten.compact
168
+ end
169
+
170
+ def flatten_catchers(object)
171
+ if object.is_a?(MethodCatcher)
172
+ consumed_catcher object
173
+ array = [
174
+ flatten_catchers(object._object),
175
+ flatten_catchers(object._method)
176
+ ]
177
+ if object._args && !object._args.empty?
178
+ array.concat object._args.map { |a| flatten_catchers a }
179
+ end
180
+ array
181
+ else
182
+ object
183
+ end
184
+ end
185
+
186
+ def consumed_catcher(c)
187
+ @catchers_hash.delete c.object_id
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,385 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'keymap/dsl_reader'
4
+
5
+ module SublimeDSL
6
+ module SublimeText
7
+
8
+ class KeyMap
9
+
10
+ def self.import(file)
11
+ name = File.basename(file, File.extname(file))
12
+ kb = Keyboard.sublime
13
+ map = new(name, kb)
14
+ list = JSON[File.read(file, encoding: 'utf-8')]
15
+ begin
16
+ map.bindings.concat list.map { |h| KeyBinding.from_json(h) }
17
+ rescue => ex
18
+ Console.error "file: #{file}"
19
+ raise ex
20
+ end
21
+ map
22
+ end
23
+
24
+ attr_reader :name, :os, :keyboard, :bindings
25
+
26
+ def initialize(name, keyboard)
27
+ @name = name
28
+ if name =~ /\((Windows|OSX|Linux)\)/
29
+ @os = $1
30
+ else
31
+ @os = nil
32
+ end
33
+
34
+ @keyboard = keyboard
35
+ @bindings = []
36
+ end
37
+
38
+ alias to_s name
39
+
40
+ def for_keyboard(other_keyboard)
41
+ return self if keyboard == other_keyboard
42
+ # do not convert if the OS do not match
43
+ return self if os && other_keyboard.os && os != other_keyboard.os
44
+ KeyMap.new(name, other_keyboard).tap do |map|
45
+ bindings.each do |b|
46
+ map.bindings << b.for_keyboard(other_keyboard)
47
+ end
48
+ end
49
+ end
50
+
51
+ def write(dir)
52
+ update_fixmes
53
+ file = "#{dir}/#{name}.keymap.rb"
54
+ File.open(file, 'wb:utf-8') do |f|
55
+ f.puts "\nkeymap #{name.to_source} do"
56
+ f.puts
57
+ f.puts " keyboard #{keyboard.name.to_source}" unless keyboard == Keyboard.sublime
58
+ f.puts " conditionals if: 'si', and: 'et', or: 'ou'"
59
+ f.puts
60
+ bindings.each do |b|
61
+ begin
62
+ f.puts ' ' << b.to_dsl.gsub("\n", "\n ")
63
+ rescue => ex
64
+ Console.error "file: #{file}\nbinding: #{b.keystrokes}"
65
+ raise ex
66
+ end
67
+ end
68
+ f.puts "\nend"
69
+ end
70
+ end
71
+
72
+ def export(dir)
73
+ file = "#{dir}/#{name}.sublime-keymap"
74
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
75
+ end
76
+
77
+ def to_json
78
+ st_bindings = for_keyboard(Keyboard.sublime).bindings
79
+ "[\n" <<
80
+ st_bindings.map { |b| b.to_json }.join(",\n") <<
81
+ "\n]"
82
+ end
83
+
84
+ def update_fixmes
85
+ by_keystrokes_and_context = bindings.group_by { |b| [b.keystrokes, b.context] }
86
+ by_keystrokes_and_context.each_value do |binding_list|
87
+ next if binding_list.length == 1
88
+ binding_list.each do |b|
89
+ b.fixmes << "assigned #{binding_list.length} times in this keymap"
90
+ end
91
+ end
92
+ end
93
+
94
+ ##
95
+ # A key binding: one or more keystrokes, a command and an optional context
96
+
97
+ class KeyBinding
98
+
99
+ def self.from_json(json_hash)
100
+ h = json_hash.dup
101
+ keystroke_specs = h.delete('keys') or raise Error, 'no keys: ' << json_hash.inspect
102
+ keystrokes = keystroke_specs.map { |s| Keyboard.sublime.ensure_keystroke(s) }
103
+ cmd = h.delete('command') or raise Error, 'no command: ' << json_hash.inspect
104
+ command = Command.new(cmd, h.delete('args'))
105
+ context_hash = h.delete('context')
106
+ context = context_hash && Context.from_json(context_hash)
107
+ h.empty? or raise Error, 'unexpected JSON keys: ' << h.inspect
108
+ new(keystrokes, command, context)
109
+ rescue => ex
110
+ warn "error with binding #{json_hash.inspect}"
111
+ warn ex.message
112
+ raise
113
+ end
114
+
115
+ attr_reader :keystrokes, :command, :context
116
+ attr_reader :fixmes
117
+ attr_accessor :source_file
118
+
119
+ def initialize(keystrokes, command, context = nil)
120
+ @keystrokes = keystrokes
121
+ @command = command
122
+ @context = context
123
+ @fixmes = []
124
+ end
125
+
126
+ def add_condition(args)
127
+ @context ||= Context.new
128
+ @context.conditions << Context::Condition.from_dsl(args)
129
+ end
130
+
131
+ def for_keyboard(other_keyboard)
132
+ if other_keyboard == Keyboard.sublime
133
+ # the current binding is for a custom keyboard:
134
+ # get the corresponding ST keystrokes
135
+ other_keystrokes = keystrokes.map do |ks|
136
+ spec = ks.key_event || ks.chr_event
137
+ spec or raise Error, "#{ks} has no SublimeText equivalent"
138
+ other_keyboard.ensure_keystroke(spec)
139
+ end
140
+ else
141
+ # the current binding is for the sublime text keyboard:
142
+ # its keystrokes may not exist in the target keyboard
143
+ other_keystrokes = keystrokes.map do |ks|
144
+ other_keyboard.keystroke_for_sublime_spec(ks.to_spec)
145
+ end
146
+ end
147
+ KeyBinding.new(other_keystrokes, command, context)
148
+ end
149
+
150
+ def to_dsl
151
+
152
+ comments = fixmes.map { |f| "# FIXME: #{f}\n" }.join
153
+ valid = true
154
+ keystrokes.each do |ks|
155
+ if ks.type == :null
156
+ comments << "# FIXME: no equivalent for keystroke: #{ks.key_event}\n"
157
+ valid = false
158
+ next
159
+ end
160
+ next if ks.type == :char || ks.to_spec.length == 1
161
+ if ks.os_action
162
+ comments << "# FIXME: #{ks} is OS-reserved (#{ks.os_action})\n"
163
+ end
164
+ if ks.key_event.nil?
165
+ comments << "# FIXME: #{ks} is not seen by Sublime Text\n"
166
+ elsif ks.chr_event
167
+ comments << "# FIXME: #{ks} also generates the character #{ks.chr_event.to_source}\n"
168
+ end
169
+ end
170
+ spec = keystrokes.map { |ks| ks.to_spec || ks.key_event }.join(', ')
171
+ dsl = "bind #{spec.to_source}, #{command.to_dsl}\n"
172
+ dsl << context.to_dsl.indent(2) << "\n" if context
173
+ dsl.gsub!(/^/, '# ') unless valid
174
+ (comments << dsl).strip
175
+ end
176
+
177
+ def to_json
178
+ h = { 'keys' => keystrokes.map { |ks| ks.to_spec } }
179
+ h.merge! command.to_h
180
+ json = ' ' << JSON.generate(h)
181
+ return json unless context
182
+ json = json[0..-2] << %(, "context": [\n )
183
+ json << context.conditions.map(&:to_json).join(",\n ")
184
+ json << "\n ]}"
185
+ json
186
+ end
187
+
188
+ alias to_s to_dsl
189
+
190
+ include Tools::ValueEquality
191
+
192
+ def value_id
193
+ [keystrokes, command, context]
194
+ end
195
+
196
+ end
197
+
198
+ ##
199
+ # A key binding context = a series of conditions.
200
+
201
+
202
+ class Context
203
+
204
+ def self.from_json(array)
205
+ new array.map { |h| Condition.new(h['key'], h['operator'], h['operand'], h['match_all']) }
206
+ end
207
+
208
+ attr_reader :conditions
209
+
210
+ def initialize(conditions = [])
211
+ @conditions = conditions
212
+ end
213
+
214
+ def to_s
215
+ conditions.map(&:to_s).join(' && ')
216
+ end
217
+
218
+ def to_dsl
219
+ dsl = []
220
+ method = 'si'
221
+ conditions.each do |c|
222
+ c.fixmes.each { |f| dsl << f }
223
+ dsl << "#{method} #{c.to_dsl}"
224
+ method = 'et'
225
+ end
226
+ dsl.join("\n")
227
+ end
228
+
229
+ include Tools::ValueEquality
230
+ alias value_id conditions
231
+
232
+ ##
233
+ # A condition. There are 3 types of conditions:
234
+ # * <i>left_operand operator right_operand</i>
235
+ # * <i>left_operand right_operand</i>
236
+ # * <i>left_operand</i>
237
+
238
+ class Condition
239
+
240
+ # Returns a new condition from an array of captures.
241
+ def self.from_dsl(args)
242
+ passed = args.dup
243
+ match_all = args.first == :all
244
+ if match_all
245
+ args.shift
246
+ args.empty? and raise Error, "'all' is not a valid condition"
247
+ else
248
+ args.empty? and raise Error, "condition missing"
249
+ end
250
+ left = args.shift.to_s
251
+ if left == 'setting' && !args.empty?
252
+ left = 'setting.' << args.shift.to_s
253
+ end
254
+ case args.length
255
+ when 0
256
+ op = nil
257
+ right = nil
258
+ when 2
259
+ case args.first
260
+ when :'=='
261
+ if args.last.is_a?(Symbol)
262
+ op = nil
263
+ args[-1] = args.last.to_s
264
+ else
265
+ op = 'equal'
266
+ end
267
+ when :'!='
268
+ op = 'not_equal'
269
+ when :is
270
+ op = nil
271
+ when :'=~'
272
+ op = 'regex_contains'
273
+ when :'!~'
274
+ op = 'not_regex_contains'
275
+ when :regex_match, :not_regex_match
276
+ op = args.first
277
+ else
278
+ raise Error, "invalid operator: #{args.first.inspect}"
279
+ end
280
+ right = args.last
281
+ right = right.source if right.is_a?(Regexp)
282
+ else
283
+ raise Error, "expected [all.]<setting> [operator] [value]: #{passed.map(&:to_s).join(' ')}"
284
+ end
285
+ new(left, op, right, match_all)
286
+ end
287
+
288
+ attr_reader :left, :operator, :right, :match_all
289
+
290
+ def initialize(left, operator, right, match_all)
291
+ @left = left
292
+ @operator = operator && operator.to_sym
293
+ if operator && operator =~ /regex/
294
+ @right = Tools::RegexpWannabe.new(right)
295
+ else
296
+ @right = right
297
+ end
298
+ @match_all = match_all ? true : nil # normalize match_all
299
+ end
300
+
301
+ def fixmes
302
+ right && right.is_a?(Tools::RegexpWannabe) ? right.fixmes : []
303
+ end
304
+
305
+ # DSL for the condition. The 3 types are rendered as:
306
+ #
307
+ # [<i>left operator right</i>]
308
+ # Same condition, with _operator_ mapped to its ruby equivalent:
309
+ # <tt>equal</tt>:: <tt>==</tt>
310
+ # <tt>not_equal</tt>:: <tt>!=</tt>
311
+ # <tt>regex_contains</tt>:: <tt>=~</tt>
312
+ # <tt>not_regex_contains</tt>:: <tt>!~</tt>
313
+ # <tt>regex_match</tt>:: <tt>.regex_match</tt> + comment
314
+ # <tt>not_regex_match</tt>:: <tt>.not_regex_match</tt> + comment
315
+ # The comment is 'could use =~ with \A and \z'
316
+ #
317
+ # [<i>left right</i>]
318
+ # * if _right_ is a String: <tt>left == :right</tt>
319
+ # * otherwise: <tt>left is right</tt>
320
+ #
321
+ # [_left_]
322
+ # _left_
323
+ #
324
+ # If #match_all is +true+, _left_ is prefixed by "<tt>all.</tt>".
325
+
326
+ def to_dsl
327
+ ( match_all ? 'all.' : '' ) <<
328
+ left << (
329
+ case operator
330
+ when nil
331
+ if right.nil?
332
+ ''
333
+ elsif right.is_a?(String)
334
+ ' == ' + right.to_sym.inspect
335
+ else
336
+ ' is ' << right.inspect
337
+ end
338
+ when :equal
339
+ ' == ' << right.inspect
340
+ when :not_equal
341
+ ' != ' << right.inspect
342
+ when :regex_contains
343
+ ' =~ ' << right.inspect
344
+ when :not_regex_contains
345
+ ' !~ ' << right.inspect
346
+ when :regex_match
347
+ ' .regex_match ' << right.inspect(true) << ' # could use =~ with \A and \z'
348
+ when :not_regex_match
349
+ ' .not_regex_match ' << right.inspect(true) << ' # could use !~ with \A and \z'
350
+ else
351
+ raise Error, "unknown operator: #{operator.inspect} #{right.to_s}"
352
+ end
353
+ )
354
+ end
355
+
356
+ def to_h
357
+ h = { 'key' => left }
358
+ h['operator'] = operator if operator
359
+ unless right.nil?
360
+ h['operand'] = right.is_a?(Tools::RegexpWannabe) ? right.to_s(true) : right
361
+ end
362
+ h['match_all'] = match_all if match_all
363
+ h
364
+ end
365
+
366
+ def to_json
367
+ JSON.generate(to_h)
368
+ end
369
+
370
+ alias to_s to_dsl
371
+
372
+ include Tools::ValueEquality
373
+
374
+ def value_id
375
+ to_h
376
+ end
377
+
378
+ end
379
+
380
+ end
381
+
382
+ end
383
+
384
+ end
385
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Macro
6
+
7
+ def self.import(file)
8
+ # Delete to Hard EOL.sublime-macro:
9
+ # [
10
+ # {"command": "move_to", "args": {"to": "hardeol", "extend": true}},
11
+ # {"command": "add_to_kill_ring", "args": {"forward": true}},
12
+ # {"command": "right_delete"}
13
+ # ]
14
+ macro = new(File.basename(file, '.sublime-macro'))
15
+ JSON[File.read(file, encoding: 'utf-8')].each do |command_hash|
16
+ cmd = command_hash.delete('command')
17
+ cmd or raise Error, "no 'command' key in '#{file}'"
18
+ args = command_hash.delete('args')
19
+ command_hash.empty? or raise Error, 'unknown sublime-macro keys: ' << command_hash.inspect
20
+ macro.commands << Command.new(cmd, args)
21
+ end
22
+
23
+ macro
24
+ end
25
+
26
+ attr_reader :name
27
+ attr_reader :commands
28
+
29
+ def initialize(name)
30
+ @name = name
31
+ @commands = []
32
+ end
33
+
34
+ alias to_s name
35
+
36
+ def to_dsl
37
+ # macro 'Delete to Hard EOL' do
38
+ # move_to "hardeol", extend: true
39
+ # add_to_kill_ring forward: true
40
+ # right_delete
41
+ # end
42
+ dsl = "macro #{name.inspect} do\n"
43
+ commands.each { |c| dsl << " #{c.to_dsl(true)}\n" }
44
+ dsl << "end"
45
+ end
46
+
47
+ def export(dir)
48
+ file = "#{dir}/#{name}.sublime-macro"
49
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
50
+ end
51
+
52
+ def to_json
53
+ # JSON.pretty_generate(commands.map(&:to_h))
54
+ "[\n" <<
55
+ commands.map { |c| JSON.generate(c.to_h) }.join(",\n").indent(2) <<
56
+ "\n]"
57
+ end
58
+
59
+ class DSLReader
60
+
61
+ def initialize(file = nil)
62
+ @macros = []
63
+ @current_macro = nil
64
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
65
+ end
66
+
67
+ def _macros
68
+ @macros
69
+ end
70
+
71
+ def method_missing(sym, *args, &block)
72
+ @current_macro or raise Error, "'#{sym}' is invalid outside of a 'macro' block"
73
+ cmd = Command.from_method_missing(sym, args)
74
+ cmd.error and
75
+ raise Error, "macro '#{@current_macro}': #{cmd.error}"
76
+ @current_macro.commands << cmd
77
+ end
78
+
79
+ def macro(name, &block)
80
+ @current_macro and raise Error, 'macro blocks cannot be nested'
81
+ @current_macro = Macro.new(name)
82
+ instance_eval(&block)
83
+ @macros << @current_macro
84
+ @current_macro = nil
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
91
+ end