rtext 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +20 -0
- data/{README → README.rdoc} +5 -1
- data/RText_Protocol +444 -0
- data/Rakefile +10 -10
- data/lib/rtext/completer.rb +32 -26
- data/lib/rtext/context_builder.rb +113 -59
- data/lib/rtext/default_loader.rb +73 -8
- data/lib/rtext/default_service_provider.rb +30 -14
- data/lib/rtext/frontend/config.rb +58 -0
- data/lib/rtext/frontend/connector.rb +233 -0
- data/lib/rtext/frontend/connector_manager.rb +81 -0
- data/lib/rtext/frontend/context.rb +56 -0
- data/lib/rtext/generic.rb +13 -0
- data/lib/rtext/instantiator.rb +30 -7
- data/lib/rtext/language.rb +54 -27
- data/lib/rtext/link_detector.rb +57 -0
- data/lib/rtext/message_helper.rb +77 -0
- data/lib/rtext/parser.rb +19 -68
- data/lib/rtext/serializer.rb +18 -3
- data/lib/rtext/service.rb +182 -118
- data/lib/rtext/tokenizer.rb +102 -0
- data/test/completer_test.rb +327 -70
- data/test/context_builder_test.rb +671 -91
- data/test/instantiator_test.rb +153 -0
- data/test/integration/backend.out +10 -0
- data/test/integration/crash_on_request_editor.rb +12 -0
- data/test/integration/ecore_editor.rb +50 -0
- data/test/integration/frontend.log +25138 -0
- data/test/integration/model/invalid_encoding.invenc +2 -0
- data/test/integration/model/test.crash_on_request +18 -0
- data/test/integration/model/test.crashing_backend +18 -0
- data/test/integration/model/test.dont_open_socket +0 -0
- data/test/integration/model/test.invalid_cmd_line +0 -0
- data/test/integration/model/test.not_in_rtext +0 -0
- data/test/integration/model/test_large_with_errors.ect3 +43523 -0
- data/test/integration/model/test_metamodel.ect +18 -0
- data/test/integration/model/test_metamodel2.ect +5 -0
- data/test/integration/model/test_metamodel_error.ect2 +3 -0
- data/test/integration/model/test_metamodel_ok.ect2 +18 -0
- data/test/integration/test.rb +684 -0
- data/test/link_detector_test.rb +276 -0
- data/test/message_helper_test.rb +118 -0
- data/test/rtext_test.rb +4 -1
- data/test/serializer_test.rb +96 -1
- data/test/tokenizer_test.rb +125 -0
- metadata +36 -10
- data/RText_Plugin_Implementation_Guide +0 -268
- data/lib/rtext_plugin/connection_manager.rb +0 -59
@@ -13,9 +13,13 @@ class DefaultServiceProvider
|
|
13
13
|
})
|
14
14
|
end
|
15
15
|
|
16
|
+
def language
|
17
|
+
@lang
|
18
|
+
end
|
19
|
+
|
16
20
|
def load_model(options={})
|
17
21
|
if options[:on_progress]
|
18
|
-
@loader.load(:
|
22
|
+
@loader.load(:on_progress => options[:on_progress])
|
19
23
|
else
|
20
24
|
@loader.load
|
21
25
|
end
|
@@ -29,8 +33,10 @@ class DefaultServiceProvider
|
|
29
33
|
clazz = reference.eType.instanceClass
|
30
34
|
targets = @model.index.values.flatten.select{|e| e.is_a?(clazz)}
|
31
35
|
end
|
36
|
+
index = 0
|
32
37
|
targets.collect{|t|
|
33
|
-
ident = @lang.identifier_provider.call(t, context.element)
|
38
|
+
ident = @lang.identifier_provider.call(t, context.element, reference, index)
|
39
|
+
index += 1
|
34
40
|
if ident
|
35
41
|
ReferenceCompletionOption.new(ident, t.class.ecore.name)
|
36
42
|
else
|
@@ -40,13 +46,17 @@ class DefaultServiceProvider
|
|
40
46
|
end
|
41
47
|
|
42
48
|
ReferenceTarget = Struct.new(:file, :line, :display_name)
|
43
|
-
def get_reference_targets(identifier,
|
49
|
+
def get_reference_targets(identifier, element, feature, index)
|
44
50
|
result = []
|
45
|
-
|
51
|
+
urefs = [
|
52
|
+
RGen::Instantiator::ReferenceResolver::UnresolvedReference.new(element, feature.name,
|
53
|
+
element.getGenericAsArray(feature.name)[index]) ]
|
54
|
+
@lang.reference_qualifier.call(urefs, @model)
|
55
|
+
identifier = urefs.first.proxy.targetIdentifier
|
46
56
|
targets = @model.index[identifier]
|
47
57
|
if targets && @lang.per_type_identifier
|
48
|
-
if
|
49
|
-
targets = targets.select{|t| t.is_a?(
|
58
|
+
if feature
|
59
|
+
targets = targets.select{|t| t.is_a?(feature.eType.instanceClass)}
|
50
60
|
end
|
51
61
|
end
|
52
62
|
targets && targets.each do |t|
|
@@ -58,22 +68,26 @@ class DefaultServiceProvider
|
|
58
68
|
result
|
59
69
|
end
|
60
70
|
|
61
|
-
def get_referencing_elements(identifier,
|
71
|
+
def get_referencing_elements(identifier, element, feature, index)
|
62
72
|
result = []
|
63
|
-
targets = @model.index[@lang.identifier_provider.call(
|
73
|
+
targets = @model.index[@lang.identifier_provider.call(element, nil, nil, nil)]
|
64
74
|
if targets && @lang.per_type_identifier
|
65
|
-
targets = targets.select{|t| t.class ==
|
75
|
+
targets = targets.select{|t| t.class == element.class}
|
66
76
|
end
|
67
77
|
if targets && targets.size == 1
|
68
78
|
target = targets.first
|
69
|
-
|
70
|
-
r.eOpposite && !r.containment && !r.eOpposite.containment}
|
71
|
-
|
79
|
+
refs = target.class.ecore.eAllReferences.select{|r|
|
80
|
+
r.eOpposite && !r.containment && !r.eOpposite.containment}
|
81
|
+
# we only want references configured in the RText language that point to this element
|
82
|
+
# thus we don't follow references here which are configured in the language
|
83
|
+
# (because for those the other direction is not configured in the language)
|
84
|
+
refs -= @lang.non_containments(target.class.ecore)
|
85
|
+
elements = refs.collect{|r| target.getGenericAsArray(r.name)}.flatten
|
72
86
|
elements.each do |e|
|
73
87
|
if @lang.fragment_ref(e)
|
74
88
|
path = File.expand_path(@lang.fragment_ref(e).fragment.location)
|
75
89
|
display_name = ""
|
76
|
-
ident = @lang.identifier_provider.call(e, nil)
|
90
|
+
ident = @lang.identifier_provider.call(e, nil, nil, nil)
|
77
91
|
display_name += "#{ident} " if ident
|
78
92
|
display_name += "[#{e.class.ecore.name}]"
|
79
93
|
result << ReferenceTarget.new(path, @lang.line_number(e), display_name)
|
@@ -148,7 +162,9 @@ class DefaultServiceProvider
|
|
148
162
|
return @element_name_index if @element_name_index
|
149
163
|
@element_name_index = {}
|
150
164
|
@model.index.each_pair do |ident, elements|
|
151
|
-
|
165
|
+
last_part = ident.split(/\W/).last
|
166
|
+
next unless last_part
|
167
|
+
key = last_part[0..0].downcase
|
152
168
|
@element_name_index[key] ||= {}
|
153
169
|
@element_name_index[key][ident] = elements
|
154
170
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RText
|
2
|
+
module Frontend
|
3
|
+
|
4
|
+
module Config
|
5
|
+
|
6
|
+
def self.find_service_config(file)
|
7
|
+
last_dir = nil
|
8
|
+
dir = File.expand_path(File.dirname(file))
|
9
|
+
search_pattern = file_pattern(file)
|
10
|
+
while dir != last_dir
|
11
|
+
config_file = "#{dir}/.rtext"
|
12
|
+
if File.exist?(config_file)
|
13
|
+
configs = parse_config_file(config_file)
|
14
|
+
config = configs.find{|s| s.patterns.any?{|p| p == search_pattern}}
|
15
|
+
return config if config
|
16
|
+
end
|
17
|
+
last_dir = dir
|
18
|
+
dir = File.dirname(dir)
|
19
|
+
end
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file_pattern(file)
|
24
|
+
ext = File.extname(file)
|
25
|
+
if ext.size > 0
|
26
|
+
"*#{ext}"
|
27
|
+
else
|
28
|
+
File.basename(file)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ServiceConfig = Struct.new(:file, :patterns, :command)
|
33
|
+
|
34
|
+
def self.parse_config_file(file)
|
35
|
+
configs = []
|
36
|
+
File.open(file) do |f|
|
37
|
+
lines = f.readlines
|
38
|
+
l = lines.shift
|
39
|
+
while l
|
40
|
+
if l =~ /^(.+):\s*$/
|
41
|
+
patterns = $1.split(",").collect{|s| s.strip}
|
42
|
+
l = lines.shift
|
43
|
+
if l && l =~ /\S/ && l !~ /:\s*$/
|
44
|
+
configs << ServiceConfig.new(file, patterns, l)
|
45
|
+
l = lines.shift
|
46
|
+
end
|
47
|
+
else
|
48
|
+
l = lines.shift
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
configs
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'rtext/message_helper'
|
4
|
+
|
5
|
+
module RText
|
6
|
+
module Frontend
|
7
|
+
|
8
|
+
class Connector
|
9
|
+
include Process
|
10
|
+
include RText::MessageHelper
|
11
|
+
|
12
|
+
def initialize(config, options={})
|
13
|
+
@config = config
|
14
|
+
@logger = options[:logger]
|
15
|
+
@state = :off
|
16
|
+
@invocation_id = 1
|
17
|
+
@invocations = {}
|
18
|
+
@busy = false
|
19
|
+
@busy_start_time = nil
|
20
|
+
@connection_listener = options[:connect_callback]
|
21
|
+
@outfile_provider = options[:outfile_provider]
|
22
|
+
@keep_outfile = options[:keep_outfile]
|
23
|
+
@connection_timeout = options[:connection_timeout] || 10
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute_command(obj, options={})
|
27
|
+
timeout = options[:timeout] || 5
|
28
|
+
@busy = false if @busy_start_time && (Time.now > @busy_start_time + timeout)
|
29
|
+
if @busy
|
30
|
+
do_work
|
31
|
+
:backend_busy
|
32
|
+
elsif connected?
|
33
|
+
obj["invocation_id"] = @invocation_id
|
34
|
+
obj["type"] = "request"
|
35
|
+
@socket.send(serialize_message(obj), 0)
|
36
|
+
result = nil
|
37
|
+
@busy = true
|
38
|
+
@busy_start_time = Time.now
|
39
|
+
if options[:response_callback]
|
40
|
+
@invocations[@invocation_id] = lambda do |r|
|
41
|
+
if r["type"] == "response" || r["type"] =~ /error$/
|
42
|
+
@busy = false
|
43
|
+
end
|
44
|
+
options[:response_callback].call(r)
|
45
|
+
end
|
46
|
+
@invocation_id += 1
|
47
|
+
do_work
|
48
|
+
:request_pending
|
49
|
+
else
|
50
|
+
@invocations[@invocation_id] = lambda do |r|
|
51
|
+
if r["type"] == "response" || r["type"] =~ /error$/
|
52
|
+
result = r
|
53
|
+
@busy = false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@invocation_id += 1
|
57
|
+
while !result
|
58
|
+
if Time.now > @busy_start_time + timeout
|
59
|
+
result = :timeout
|
60
|
+
@busy = false
|
61
|
+
else
|
62
|
+
sleep(0.1)
|
63
|
+
do_work
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
else
|
69
|
+
connect unless connecting?
|
70
|
+
do_work
|
71
|
+
:connecting
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def resume
|
76
|
+
do_work
|
77
|
+
end
|
78
|
+
|
79
|
+
def stop
|
80
|
+
while connecting?
|
81
|
+
do_work
|
82
|
+
sleep(0.1)
|
83
|
+
end
|
84
|
+
if connected?
|
85
|
+
execute_command({"type" => "request", "command" => "stop"})
|
86
|
+
while do_work
|
87
|
+
sleep(0.1)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def connected?
|
95
|
+
@state == :connected && backend_running?
|
96
|
+
end
|
97
|
+
|
98
|
+
def connecting?
|
99
|
+
@state == :connecting
|
100
|
+
end
|
101
|
+
|
102
|
+
def backend_running?
|
103
|
+
if @process_id
|
104
|
+
begin
|
105
|
+
return true unless waitpid(@process_id, Process::WNOHANG)
|
106
|
+
rescue Errno::ECHILD
|
107
|
+
end
|
108
|
+
end
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
def tempfile_name
|
113
|
+
dir = Dir.tmpdir
|
114
|
+
i = 0
|
115
|
+
file = nil
|
116
|
+
while !file || File.exist?(file)
|
117
|
+
file = dir+"/rtext.temp.#{i}"
|
118
|
+
i += 1
|
119
|
+
end
|
120
|
+
file
|
121
|
+
end
|
122
|
+
|
123
|
+
def connect
|
124
|
+
@state = :connecting
|
125
|
+
@connect_start_time = Time.now
|
126
|
+
|
127
|
+
@logger.info @config.command if @logger
|
128
|
+
|
129
|
+
if @outfile_provider
|
130
|
+
@out_file = @outfile_provider.call
|
131
|
+
else
|
132
|
+
@out_file = tempfile_name
|
133
|
+
end
|
134
|
+
File.unlink(@out_file) if File.exist?(@out_file)
|
135
|
+
|
136
|
+
Dir.chdir(File.dirname(@config.file)) do
|
137
|
+
@process_id = spawn(@config.command.strip + " > #{@out_file} 2>&1")
|
138
|
+
end
|
139
|
+
@work_state = :wait_for_file
|
140
|
+
end
|
141
|
+
|
142
|
+
def do_work
|
143
|
+
case @work_state
|
144
|
+
when :wait_for_file
|
145
|
+
if File.exist?(@out_file)
|
146
|
+
@work_state = :wait_for_port
|
147
|
+
end
|
148
|
+
if Time.now > @connect_start_time + @connection_timeout
|
149
|
+
cleanup
|
150
|
+
@connection_listener.call(:timeout) if @connection_listener
|
151
|
+
@work_state = :done
|
152
|
+
@state = :off
|
153
|
+
@logger.warn "process didn't startup (connection timeout)" if @logger
|
154
|
+
end
|
155
|
+
true
|
156
|
+
when :wait_for_port
|
157
|
+
output = File.read(@out_file)
|
158
|
+
if output =~ /^RText service, listening on port (\d+)/
|
159
|
+
port = $1.to_i
|
160
|
+
@logger.info "connecting to #{port}" if @logger
|
161
|
+
begin
|
162
|
+
@socket = TCPSocket.new("127.0.0.1", port)
|
163
|
+
rescue Errno::ECONNREFUSED
|
164
|
+
cleanup
|
165
|
+
@connection_listener.call(:timeout) if @connection_listener
|
166
|
+
@work_state = :done
|
167
|
+
@state = :off
|
168
|
+
@logger.warn "could not connect socket (connection timeout)" if @logger
|
169
|
+
end
|
170
|
+
@state = :connected
|
171
|
+
@work_state = :read_from_socket
|
172
|
+
@connection_listener.call(:connected) if @connection_listener
|
173
|
+
end
|
174
|
+
if Time.now > @connect_start_time + @connection_timeout
|
175
|
+
cleanup
|
176
|
+
@connection_listener.call(:timeout) if @connection_listener
|
177
|
+
@work_state = :done
|
178
|
+
@state = :off
|
179
|
+
@logger.warn "could not connect socket (connection timeout)" if @logger
|
180
|
+
end
|
181
|
+
true
|
182
|
+
when :read_from_socket
|
183
|
+
repeat = true
|
184
|
+
socket_closed = false
|
185
|
+
response_data = ""
|
186
|
+
while repeat
|
187
|
+
repeat = false
|
188
|
+
data = nil
|
189
|
+
begin
|
190
|
+
data = @socket.read_nonblock(100000)
|
191
|
+
rescue Errno::EWOULDBLOCK
|
192
|
+
rescue IOError, EOFError, Errno::ECONNRESET
|
193
|
+
socket_closed = true
|
194
|
+
@logger.info "server socket closed (end of file)" if @logger
|
195
|
+
end
|
196
|
+
if data
|
197
|
+
repeat = true
|
198
|
+
response_data.concat(data)
|
199
|
+
while obj = extract_message(response_data)
|
200
|
+
inv_id = obj["invocation_id"]
|
201
|
+
callback = @invocations[inv_id]
|
202
|
+
if callback
|
203
|
+
callback.call(obj)
|
204
|
+
else
|
205
|
+
@logger.error "unknown answer" if @logger
|
206
|
+
end
|
207
|
+
end
|
208
|
+
elsif !backend_running? || socket_closed
|
209
|
+
cleanup
|
210
|
+
@work_state = :done
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
def cleanup
|
220
|
+
@socket.close if @socket
|
221
|
+
# wait up to 2 seconds for backend to shutdown
|
222
|
+
for i in 0..20
|
223
|
+
break unless backend_running?
|
224
|
+
sleep(0.1)
|
225
|
+
end
|
226
|
+
File.unlink(@out_file) unless @keep_outfile
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'rtext/frontend/config'
|
3
|
+
require 'rtext/frontend/connector'
|
4
|
+
|
5
|
+
module RText
|
6
|
+
module Frontend
|
7
|
+
|
8
|
+
class ConnectorManager
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
@logger = options[:logger]
|
12
|
+
@connector_descs = {}
|
13
|
+
@connector_listener = options[:connect_callback]
|
14
|
+
@keep_outfile = options[:keep_outfile]
|
15
|
+
@outfile_provider = options[:outfile_provider]
|
16
|
+
@connection_timeout = options[:connection_timeout]
|
17
|
+
end
|
18
|
+
|
19
|
+
ConnectorDesc = Struct.new(:connector, :checksum)
|
20
|
+
|
21
|
+
def connector_for_file(file)
|
22
|
+
config = Config.find_service_config(file)
|
23
|
+
if config
|
24
|
+
file_pattern = Config.file_pattern(file)
|
25
|
+
key = desc_key(config, file_pattern)
|
26
|
+
desc = @connector_descs[key]
|
27
|
+
if desc
|
28
|
+
if desc.checksum == config_checksum(config)
|
29
|
+
desc.connector
|
30
|
+
else
|
31
|
+
# connector must be replaced
|
32
|
+
desc.connector.stop
|
33
|
+
create_connector(config, file_pattern)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
create_connector(config, file_pattern)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def all_connectors
|
44
|
+
@connector_descs.values.collect{|v| v.connector}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def create_connector(config, pattern)
|
50
|
+
con = Connector.new(config, :logger => @logger, :keep_outfile => @keep_outfile,
|
51
|
+
:outfile_provider => @outfile_provider,
|
52
|
+
:connection_timeout => @connection_timeout,
|
53
|
+
:connect_callback => lambda do |state|
|
54
|
+
@connector_listener.call(con, state) if @connector_listener
|
55
|
+
end)
|
56
|
+
desc = ConnectorDesc.new(con, config_checksum(config))
|
57
|
+
key = desc_key(config, pattern)
|
58
|
+
@connector_descs[key] = desc
|
59
|
+
desc.connector
|
60
|
+
end
|
61
|
+
|
62
|
+
def desc_key(config, pattern)
|
63
|
+
config.file.downcase + "," + pattern
|
64
|
+
end
|
65
|
+
|
66
|
+
def config_checksum(config)
|
67
|
+
if File.exist?(config.file)
|
68
|
+
sha1 = Digest::SHA1.new
|
69
|
+
sha1.file(config.file)
|
70
|
+
sha1.hexdigest
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|