rtext 0.4.0 → 0.5.0
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.
- 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
|
+
|