rtext 0.8.1 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG +120 -89
- data/Project.yaml +15 -0
- data/RText_Protocol +47 -4
- data/lib/rtext/context_builder.rb +49 -8
- data/lib/rtext/default_completer.rb +212 -163
- data/lib/rtext/default_service_provider.rb +3 -3
- data/lib/rtext/frontend/connector.rb +130 -56
- data/lib/rtext/instantiator.rb +11 -3
- data/lib/rtext/language.rb +5 -5
- data/lib/rtext/serializer.rb +3 -3
- data/lib/rtext/service.rb +281 -253
- data/lib/rtext/tokenizer.rb +2 -2
- metadata +33 -33
- data/Rakefile +0 -46
- data/test/completer_test.rb +0 -606
- data/test/context_builder_test.rb +0 -948
- data/test/frontend/context_test.rb +0 -301
- data/test/instantiator_test.rb +0 -1704
- data/test/integration/backend.out +0 -13
- data/test/integration/crash_on_request_editor.rb +0 -12
- data/test/integration/ecore_editor.rb +0 -50
- data/test/integration/frontend.log +0 -38203
- data/test/integration/model/invalid_encoding.invenc +0 -2
- data/test/integration/model/test.crash_on_request +0 -18
- data/test/integration/model/test.crashing_backend +0 -18
- 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 +0 -43523
- data/test/integration/model/test_metamodel.ect +0 -24
- data/test/integration/model/test_metamodel2.ect +0 -5
- data/test/integration/model/test_metamodel3.ect4 +0 -7
- data/test/integration/model/test_metamodel_error.ect2 +0 -3
- data/test/integration/model/test_metamodel_ok.ect2 +0 -18
- data/test/integration/test.rb +0 -966
- data/test/link_detector_test.rb +0 -287
- data/test/message_helper_test.rb +0 -118
- data/test/rtext_test.rb +0 -11
- data/test/serializer_test.rb +0 -1004
- data/test/tokenizer_test.rb +0 -173
data/lib/rtext/service.rb
CHANGED
@@ -1,253 +1,281 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require 'rtext/
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# :
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# :
|
27
|
-
# a
|
28
|
-
#
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
begin
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
{
|
207
|
-
end
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
end
|
251
|
-
|
252
|
-
|
253
|
-
|
1
|
+
require 'socket'
|
2
|
+
require 'yaml'
|
3
|
+
require 'filelock'
|
4
|
+
require 'rtext/context_builder'
|
5
|
+
require 'rtext/message_helper'
|
6
|
+
require 'rtext/link_detector'
|
7
|
+
|
8
|
+
# optimization: garbage collect while service is idle
|
9
|
+
|
10
|
+
module RText
|
11
|
+
|
12
|
+
class Service
|
13
|
+
include RText::MessageHelper
|
14
|
+
|
15
|
+
PortRangeStart = 9001
|
16
|
+
PortRangeEnd = 9100
|
17
|
+
|
18
|
+
FlushInterval = 1
|
19
|
+
ProtocolVersion = 1
|
20
|
+
|
21
|
+
# Creates an RText backend service. Options:
|
22
|
+
#
|
23
|
+
# :timeout
|
24
|
+
# idle time in seconds after which the service will terminate itself
|
25
|
+
#
|
26
|
+
# :logger
|
27
|
+
# a logger object on which the service will write its logging output
|
28
|
+
#
|
29
|
+
# :on_startup:
|
30
|
+
# a Proc which is called right after the service has started up
|
31
|
+
# can be used to output version information
|
32
|
+
#
|
33
|
+
def initialize(service_provider, options={})
|
34
|
+
@service_provider = service_provider
|
35
|
+
@timeout = options[:timeout] || 60
|
36
|
+
@logger = options[:logger]
|
37
|
+
@on_startup = options[:on_startup]
|
38
|
+
@lock_file_path = options[:lock_file_path] || File.expand_path('.rtext.lock')
|
39
|
+
@config_file_path = options[:config_file_path] || File.expand_path('.rtext.config')
|
40
|
+
@lock_file = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
Filelock @lock_file_path, :timeout => 0, :wait => 1 do
|
45
|
+
server = create_server
|
46
|
+
puts "RText service, listening on port #{server.addr[1]}"
|
47
|
+
@logger.debug('Server started') if @logger
|
48
|
+
File.write(@config_file_path, YAML.dump({'port' => server.addr[1], 'pid' => Process.pid}))
|
49
|
+
begin
|
50
|
+
@on_startup.call if @on_startup
|
51
|
+
$stdout.flush
|
52
|
+
|
53
|
+
last_access_time = Time.now
|
54
|
+
last_flush_time = Time.now
|
55
|
+
@stop_requested = false
|
56
|
+
sockets = []
|
57
|
+
request_data = {}
|
58
|
+
while !@stop_requested
|
59
|
+
begin
|
60
|
+
sock = server.accept_nonblock
|
61
|
+
sock.sync = true
|
62
|
+
sockets << sock
|
63
|
+
@logger.info "accepted connection" if @logger
|
64
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR, Errno::EWOULDBLOCK
|
65
|
+
rescue Exception => e
|
66
|
+
@logger.warn "unexpected exception during socket accept: #{e.class}"
|
67
|
+
end
|
68
|
+
sockets.dup.each do |sock|
|
69
|
+
data = nil
|
70
|
+
begin
|
71
|
+
data = sock.read_nonblock(100000)
|
72
|
+
@logger.debug('Got data') if @logger
|
73
|
+
rescue Errno::EWOULDBLOCK
|
74
|
+
rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
|
75
|
+
sock.close
|
76
|
+
request_data[sock] = nil
|
77
|
+
sockets.delete(sock)
|
78
|
+
rescue Exception => e
|
79
|
+
# catch Exception to make sure we don't crash due to unexpected exceptions
|
80
|
+
@logger.warn "unexpected exception during socket read: #{e.class}"
|
81
|
+
sock.close
|
82
|
+
request_data[sock] = nil
|
83
|
+
sockets.delete(sock)
|
84
|
+
end
|
85
|
+
if data
|
86
|
+
last_access_time = Time.now
|
87
|
+
request_data[sock] ||= ""
|
88
|
+
request_data[sock].concat(data)
|
89
|
+
@logger.debug("Data available: #{request_data[sock]}") if @logger
|
90
|
+
while obj = extract_message(request_data[sock])
|
91
|
+
@logger.debug("Got message #{obj.inspect}") if @logger
|
92
|
+
message_received(sock, obj)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
IO.select([server] + sockets, [], [], 1)
|
97
|
+
if Time.now > last_access_time + @timeout
|
98
|
+
@logger.info("RText service, stopping now (timeout)") if @logger
|
99
|
+
break
|
100
|
+
end
|
101
|
+
if Time.now > last_flush_time + FlushInterval
|
102
|
+
$stdout.flush
|
103
|
+
last_flush_time = Time.now
|
104
|
+
end
|
105
|
+
end
|
106
|
+
ensure
|
107
|
+
@logger.warn("Config file doesn't exist, but lock is acquired, it could be a bug") unless File.exist?(@config_file_path)
|
108
|
+
File.unlink(@config_file_path) if File.exist?(@config_file_path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def message_received(sock, obj)
|
114
|
+
if check_request(obj)
|
115
|
+
request_start = Time.now
|
116
|
+
@logger.debug("request: "+obj.inspect) if @logger
|
117
|
+
response = { "type" => "response", "invocation_id" => obj["invocation_id"] }
|
118
|
+
case obj["command"]
|
119
|
+
when "version"
|
120
|
+
version(sock, obj, response)
|
121
|
+
when "load_model"
|
122
|
+
load_model(sock, obj, response)
|
123
|
+
when "content_complete"
|
124
|
+
content_complete(sock, obj, response)
|
125
|
+
when "link_targets"
|
126
|
+
link_targets(sock, obj, response)
|
127
|
+
when "find_elements"
|
128
|
+
find_elements(sock, obj, response)
|
129
|
+
when "stop"
|
130
|
+
@logger.info("RText service, stopping now (stop requested)") if @logger
|
131
|
+
@stop_requested = true
|
132
|
+
else
|
133
|
+
@logger.warn("unknown command #{obj["command"]}") if @logger
|
134
|
+
response["type"] = "unknown_command_error"
|
135
|
+
response["command"] = obj["command"]
|
136
|
+
end
|
137
|
+
@logger.debug("response: "+truncate_response_for_debug_output(response).inspect) \
|
138
|
+
if response && @logger
|
139
|
+
send_response(sock, response)
|
140
|
+
@logger.info("request complete (#{Time.now-request_start}s)")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def check_request(obj)
|
147
|
+
if obj["type"] != "request"
|
148
|
+
@logger.warn("received message is not a request") if @logger
|
149
|
+
false
|
150
|
+
elsif !obj["invocation_id"].is_a?(Integer)
|
151
|
+
@logger.warn("invalid invocation id #{obj["invocation_id"]}") if @logger
|
152
|
+
false
|
153
|
+
else
|
154
|
+
true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def version(sock, request, response)
|
159
|
+
response["version"] = ProtocolVersion
|
160
|
+
end
|
161
|
+
|
162
|
+
def load_model(sock, request, response)
|
163
|
+
problems = @service_provider.get_problems(
|
164
|
+
:on_progress => lambda do |frag, work_done, work_overall|
|
165
|
+
work_overall = 1 if work_overall < 1
|
166
|
+
work_done = work_overall if work_done > work_overall
|
167
|
+
work_done = 0 if work_done < 0
|
168
|
+
send_response(sock, {
|
169
|
+
"type" => "progress",
|
170
|
+
"invocation_id" => request["invocation_id"],
|
171
|
+
"percentage" => work_done*100/work_overall
|
172
|
+
})
|
173
|
+
end)
|
174
|
+
total = 0
|
175
|
+
response["problems"] = problems.collect do |fp|
|
176
|
+
{ "file" => fp.file,
|
177
|
+
"problems" => fp.problems.collect do |p|
|
178
|
+
total += 1
|
179
|
+
{ "severity" => "error", "line" => p.line, "message" => p.message }
|
180
|
+
end }
|
181
|
+
end
|
182
|
+
response["total_problems"] = total
|
183
|
+
end
|
184
|
+
|
185
|
+
InsertString = "insert"
|
186
|
+
DisplayString = "display"
|
187
|
+
DescriptionString = "desc"
|
188
|
+
|
189
|
+
def content_complete(sock, request, response)
|
190
|
+
# column numbers start at 1
|
191
|
+
linepos = request["column"]-1
|
192
|
+
lines = request["context"]
|
193
|
+
version = request["version"].to_i
|
194
|
+
lang = @service_provider.language
|
195
|
+
response["options"] = []
|
196
|
+
return unless lang
|
197
|
+
context = ContextBuilder.build_context(lang, lines, linepos)
|
198
|
+
@logger.debug("context element: #{lang.identifier_provider.call(context.element, nil, nil, nil)}") \
|
199
|
+
if context && context.element && @logger
|
200
|
+
if @service_provider.method(:get_completion_options).arity == 1
|
201
|
+
options = @service_provider.get_completion_options(context)
|
202
|
+
else
|
203
|
+
options = @service_provider.get_completion_options(context, version)
|
204
|
+
end
|
205
|
+
response["options"] = options.collect do |o|
|
206
|
+
{ InsertString => o.insert, DisplayString => o.display, DescriptionString => o.description }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def link_targets(sock, request, response)
|
211
|
+
# column numbers start at 1
|
212
|
+
linepos = request["column"]
|
213
|
+
lines = request["context"]
|
214
|
+
lang = @service_provider.language
|
215
|
+
response["targets"] = []
|
216
|
+
return unless lang
|
217
|
+
link_descriptor = RText::LinkDetector.new(lang).detect(lines, linepos)
|
218
|
+
if link_descriptor
|
219
|
+
response["begin_column"] = link_descriptor.scol
|
220
|
+
response["end_column"] = link_descriptor.ecol
|
221
|
+
targets = []
|
222
|
+
@service_provider.get_link_targets(link_descriptor).each do |t|
|
223
|
+
targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
|
224
|
+
end
|
225
|
+
response["targets"] = targets
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def find_elements(sock, request, response)
|
230
|
+
pattern = request["search_pattern"]
|
231
|
+
total = 0
|
232
|
+
response["elements"] = @service_provider.get_open_element_choices(pattern).collect do |c|
|
233
|
+
total += 1
|
234
|
+
{ "display" => c.display_name, "file" => c.file, "line" => c.line }
|
235
|
+
end
|
236
|
+
response["total_elements"] = total
|
237
|
+
end
|
238
|
+
|
239
|
+
def send_response(sock, response)
|
240
|
+
if response
|
241
|
+
begin
|
242
|
+
sock.write(serialize_message(response))
|
243
|
+
sock.flush
|
244
|
+
# if there is an exception, the next read should shutdown the connection properly
|
245
|
+
rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
|
246
|
+
rescue Exception => e
|
247
|
+
# catch Exception to make sure we don't crash due to unexpected exceptions
|
248
|
+
@logger.warn "unexpected exception during socket write: #{e.class}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def truncate_response_for_debug_output(response_hash)
|
254
|
+
result = {}
|
255
|
+
response_hash.each_pair do |k,v|
|
256
|
+
if v.is_a?(Array) && v.size > 100
|
257
|
+
result[k] = v[0..99] + ["<truncated>"]
|
258
|
+
else
|
259
|
+
result[k] = v
|
260
|
+
end
|
261
|
+
end
|
262
|
+
result
|
263
|
+
end
|
264
|
+
|
265
|
+
def create_server
|
266
|
+
port = PortRangeStart
|
267
|
+
serv = nil
|
268
|
+
begin
|
269
|
+
serv = TCPServer.new("127.0.0.1", port)
|
270
|
+
rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT, Errno::EACCES
|
271
|
+
port += 1
|
272
|
+
retry if port <= PortRangeEnd
|
273
|
+
raise
|
274
|
+
end
|
275
|
+
serv
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
data/lib/rtext/tokenizer.rb
CHANGED
@@ -49,7 +49,7 @@ module Tokenizer
|
|
49
49
|
if val.size <= 16
|
50
50
|
val = val.to_f
|
51
51
|
else
|
52
|
-
val = BigDecimal
|
52
|
+
val = BigDecimal(val)
|
53
53
|
end
|
54
54
|
result << Token.new(:float, val, idx, col, col+$&.size-1)
|
55
55
|
col += $&.size
|
@@ -77,7 +77,7 @@ module Tokenizer
|
|
77
77
|
str = $'
|
78
78
|
result << Token.new(:boolean, $& == "true", idx, col, col+$&.size-1)
|
79
79
|
col += $&.size
|
80
|
-
when /\A([a-zA-Z_]\w*)\b
|
80
|
+
when /\A([a-zA-Z_]\w*)\b:?/
|
81
81
|
str = $'
|
82
82
|
if $&[-1] == ?:
|
83
83
|
result << Token.new(:label, $1, idx, col, col+$&.size-1)
|