rtext 0.8.2 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +116 -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 +8 -4
  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 +1 -1
  14. metadata +32 -44
  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 -1735
  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 -40502
  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
+
@@ -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)