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