sublime_dsl 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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