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.
- 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
|