sublime_dsl 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/Rakefile +248 -0
- data/SYNTAX.md +927 -0
- data/bin/subdsl +4 -0
- data/lib/sublime_dsl/cli/export.rb +134 -0
- data/lib/sublime_dsl/cli/import.rb +143 -0
- data/lib/sublime_dsl/cli.rb +125 -0
- data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
- data/lib/sublime_dsl/core_ext/string.rb +129 -0
- data/lib/sublime_dsl/core_ext.rb +4 -0
- data/lib/sublime_dsl/sublime_text/command.rb +157 -0
- data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
- data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
- data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
- data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
- data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
- data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
- data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
- data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
- data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
- data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
- data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
- data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
- data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
- data/lib/sublime_dsl/sublime_text/package.rb +96 -0
- data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
- data/lib/sublime_dsl/sublime_text.rb +48 -0
- data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
- data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
- data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
- data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
- data/lib/sublime_dsl/textmate/grammar.rb +252 -0
- data/lib/sublime_dsl/textmate/plist.rb +141 -0
- data/lib/sublime_dsl/textmate/preference.rb +301 -0
- data/lib/sublime_dsl/textmate/snippet.rb +437 -0
- data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
- data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
- data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
- data/lib/sublime_dsl/textmate/theme.rb +364 -0
- data/lib/sublime_dsl/textmate.rb +9 -0
- data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
- data/lib/sublime_dsl/tools/console.rb +74 -0
- data/lib/sublime_dsl/tools/helpers.rb +152 -0
- data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
- data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
- data/lib/sublime_dsl/tools/value_equality.rb +37 -0
- data/lib/sublime_dsl/tools/xml.rb +66 -0
- data/lib/sublime_dsl/tools.rb +66 -0
- data/lib/sublime_dsl.rb +23 -0
- 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
|