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.
Files changed (48) hide show
  1. data/CHANGELOG +20 -0
  2. data/{README → README.rdoc} +5 -1
  3. data/RText_Protocol +444 -0
  4. data/Rakefile +10 -10
  5. data/lib/rtext/completer.rb +32 -26
  6. data/lib/rtext/context_builder.rb +113 -59
  7. data/lib/rtext/default_loader.rb +73 -8
  8. data/lib/rtext/default_service_provider.rb +30 -14
  9. data/lib/rtext/frontend/config.rb +58 -0
  10. data/lib/rtext/frontend/connector.rb +233 -0
  11. data/lib/rtext/frontend/connector_manager.rb +81 -0
  12. data/lib/rtext/frontend/context.rb +56 -0
  13. data/lib/rtext/generic.rb +13 -0
  14. data/lib/rtext/instantiator.rb +30 -7
  15. data/lib/rtext/language.rb +54 -27
  16. data/lib/rtext/link_detector.rb +57 -0
  17. data/lib/rtext/message_helper.rb +77 -0
  18. data/lib/rtext/parser.rb +19 -68
  19. data/lib/rtext/serializer.rb +18 -3
  20. data/lib/rtext/service.rb +182 -118
  21. data/lib/rtext/tokenizer.rb +102 -0
  22. data/test/completer_test.rb +327 -70
  23. data/test/context_builder_test.rb +671 -91
  24. data/test/instantiator_test.rb +153 -0
  25. data/test/integration/backend.out +10 -0
  26. data/test/integration/crash_on_request_editor.rb +12 -0
  27. data/test/integration/ecore_editor.rb +50 -0
  28. data/test/integration/frontend.log +25138 -0
  29. data/test/integration/model/invalid_encoding.invenc +2 -0
  30. data/test/integration/model/test.crash_on_request +18 -0
  31. data/test/integration/model/test.crashing_backend +18 -0
  32. data/test/integration/model/test.dont_open_socket +0 -0
  33. data/test/integration/model/test.invalid_cmd_line +0 -0
  34. data/test/integration/model/test.not_in_rtext +0 -0
  35. data/test/integration/model/test_large_with_errors.ect3 +43523 -0
  36. data/test/integration/model/test_metamodel.ect +18 -0
  37. data/test/integration/model/test_metamodel2.ect +5 -0
  38. data/test/integration/model/test_metamodel_error.ect2 +3 -0
  39. data/test/integration/model/test_metamodel_ok.ect2 +18 -0
  40. data/test/integration/test.rb +684 -0
  41. data/test/link_detector_test.rb +276 -0
  42. data/test/message_helper_test.rb +118 -0
  43. data/test/rtext_test.rb +4 -1
  44. data/test/serializer_test.rb +96 -1
  45. data/test/tokenizer_test.rb +125 -0
  46. metadata +36 -10
  47. data/RText_Plugin_Implementation_Guide +0 -268
  48. data/lib/rtext_plugin/connection_manager.rb +0 -59
@@ -1,4 +1,5 @@
1
1
  require 'rtext/language'
2
+ require 'rtext/generic'
2
3
 
3
4
  module RText
4
5
 
@@ -57,6 +58,13 @@ class Serializer
57
58
  write("##{l}")
58
59
  end
59
60
  end
61
+ # the annotation provider may modify the element
62
+ annotation = @lang.annotation_provider && @lang.annotation_provider.call(element)
63
+ if annotation
64
+ annotation.split(/\r?\n/).each do |l|
65
+ write("@#{l}")
66
+ end
67
+ end
60
68
  headline = @lang.command_by_class(clazz.instanceClass)
61
69
  raise "no command name for class #{clazz.instanceClass.to_s}" unless headline
62
70
  args = []
@@ -110,8 +118,15 @@ class Serializer
110
118
  values = element.getGenericAsArray(feature.name).compact
111
119
  result = []
112
120
  arg_format = @lang.argument_format(feature)
113
- values.each do |v|
114
- if feature.eType.instanceClass == Integer
121
+ values.each_with_index do |v, index|
122
+ if v.is_a?(RText::Generic)
123
+ str = v.string.split("%>").first
124
+ if str.index(">")
125
+ result << "<%#{str}%>"
126
+ else
127
+ result << "<#{str}>"
128
+ end
129
+ elsif feature.eType.instanceClass == Integer
115
130
  if arg_format
116
131
  result << sprintf(arg_format, v)
117
132
  else
@@ -147,7 +162,7 @@ class Serializer
147
162
  gsub("\r","\\r").gsub("\t","\\t").gsub("\f","\\f").gsub("\b","\\b")}\""
148
163
  end
149
164
  elsif feature.is_a?(RGen::ECore::EReference)
150
- result << @lang.identifier_provider.call(v, element)
165
+ result << @lang.identifier_provider.call(v, element, feature, index)
151
166
  end
152
167
  end
153
168
  if result.size > 1
@@ -1,10 +1,14 @@
1
1
  require 'socket'
2
2
  require 'rtext/completer'
3
3
  require 'rtext/context_builder'
4
+ require 'rtext/message_helper'
5
+ require 'rtext/link_detector'
4
6
 
5
7
  module RText
6
8
 
7
9
  class Service
10
+ include RText::MessageHelper
11
+
8
12
  PortRangeStart = 9001
9
13
  PortRangeEnd = 9100
10
14
 
@@ -18,170 +22,230 @@ class Service
18
22
  # :logger
19
23
  # a logger object on which the service will write its logging output
20
24
  #
21
- def initialize(lang, service_provider, options={})
22
- @lang = lang
25
+ # :on_startup:
26
+ # a Proc which is called right after the service has started up
27
+ # can be used to output version information
28
+ #
29
+ def initialize(service_provider, options={})
23
30
  @service_provider = service_provider
24
- @completer = RText::Completer.new(lang)
25
31
  @timeout = options[:timeout] || 60
26
32
  @logger = options[:logger]
33
+ @on_startup = options[:on_startup]
27
34
  end
28
35
 
29
36
  def run
30
- socket = create_socket
31
- puts "RText service, listening on port #{socket.addr[1]}"
37
+ server = create_server
38
+ puts "RText service, listening on port #{server.addr[1]}"
39
+ @on_startup.call if @on_startup
32
40
  $stdout.flush
33
41
 
34
42
  last_access_time = Time.now
35
43
  last_flush_time = Time.now
36
- stop_requested = false
37
- while !stop_requested
44
+ @stop_requested = false
45
+ sockets = []
46
+ request_data = {}
47
+ while !@stop_requested
38
48
  begin
39
- msg, from = socket.recvfrom_nonblock(65000)
40
- rescue Errno::EWOULDBLOCK
41
- sleep(0.01)
42
- if (Time.now - last_access_time) > @timeout
43
- @logger.info("RText service, stopping now (timeout)") if @logger
44
- break
49
+ sock = server.accept_nonblock
50
+ sock.sync = true
51
+ sockets << sock
52
+ @logger.info "accepted connection" if @logger
53
+ rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR, Errno::EWOULDBLOCK
54
+ rescue Exception => e
55
+ @logger.warn "unexpected exception during socket accept: #{e.class}"
56
+ end
57
+ sockets.dup.each do |sock|
58
+ data = nil
59
+ begin
60
+ data = sock.read_nonblock(100000)
61
+ rescue Errno::EWOULDBLOCK
62
+ rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
63
+ sock.close
64
+ request_data[sock] = nil
65
+ sockets.delete(sock)
66
+ rescue Exception => e
67
+ # catch Exception to make sure we don't crash due to unexpected exceptions
68
+ @logger.warn "unexpected exception during socket read: #{e.class}"
69
+ sock.close
70
+ request_data[sock] = nil
71
+ sockets.delete(sock)
45
72
  end
46
- retry
73
+ if data
74
+ last_access_time = Time.now
75
+ request_data[sock] ||= ""
76
+ request_data[sock].concat(data)
77
+ while obj = extract_message(request_data[sock])
78
+ message_received(sock, obj)
79
+ end
80
+ end
81
+ end
82
+ IO.select([server] + sockets, [], [], 1)
83
+ if Time.now > last_access_time + @timeout
84
+ @logger.info("RText service, stopping now (timeout)") if @logger
85
+ break
47
86
  end
48
87
  if Time.now > last_flush_time + FlushInterval
49
88
  $stdout.flush
50
89
  last_flush_time = Time.now
51
90
  end
52
- last_access_time = Time.now
53
- lines = msg.split(/\r?\n/)
54
- cmd = lines.shift
55
- invocation_id = lines.shift
56
- response = nil
57
- progress_index = 0
58
- case cmd
59
- when "protocol_version"
60
- response = ["1"]
61
- when "refresh"
62
- response = refresh(lines)
63
- when "complete"
64
- response = complete(lines)
65
- when "show_problems"
66
- response = get_problems(lines)
67
- when "show_problems2"
68
- response = get_problems(lines, :with_severity => true, :on_progress => lambda do |frag, num_frags|
69
- progress_index += 1
70
- num_frags = 1 if num_frags < 1
71
- progress = ["progress: #{progress_index*100/num_frags}"]
72
- send_response(progress, invocation_id, socket, from, :incremental => true)
73
- end)
74
- when "get_reference_targets"
75
- response = get_reference_targets(lines)
76
- when "get_elements"
77
- response = get_open_element_choices(lines)
91
+ end
92
+ end
93
+
94
+ def message_received(sock, obj)
95
+ if check_request(obj)
96
+ @logger.debug("request: "+obj.inspect) if @logger
97
+ response = { "type" => "response", "invocation_id" => obj["invocation_id"] }
98
+ case obj["command"]
99
+ when "load_model"
100
+ load_model(sock, obj, response)
101
+ when "content_complete"
102
+ content_complete(sock, obj, response)
103
+ when "link_targets"
104
+ link_targets(sock, obj, response)
105
+ when "find_elements"
106
+ find_elements(sock, obj, response)
78
107
  when "stop"
79
- response = []
80
108
  @logger.info("RText service, stopping now (stop requested)") if @logger
81
- stop_requested = true
109
+ @stop_requested = true
82
110
  else
83
- @logger.debug("unknown command #{cmd}") if @logger
84
- response = []
111
+ @logger.warn("unknown command #{obj["command"]}") if @logger
112
+ response["type"] = "unknown_command_error"
113
+ response["command"] = obj["command"]
85
114
  end
86
- send_response(response, invocation_id, socket, from)
115
+ @logger.debug("response: "+response.inspect) if response && @logger
116
+ send_response(sock, response)
87
117
  end
88
118
  end
89
119
 
90
120
  private
91
121
 
92
- def send_response(response, invocation_id, socket, from, options={})
93
- @logger.debug(response.inspect) if @logger
94
- loop do
95
- packet_lines = []
96
- size = 0
97
- while response.size > 0 && size + response.first.size < 65000
98
- size += response.first.size
99
- packet_lines << response.shift
100
- end
101
- if options[:incremental] || response.size > 0
102
- packet_lines.unshift("more\n")
103
- else
104
- packet_lines.unshift("last\n")
105
- end
106
- packet_lines.unshift("#{invocation_id}\n")
107
- socket.send(packet_lines.join, 0, from[2], from[1])
108
- break if response.size == 0
122
+ def check_request(obj)
123
+ if obj["type"] != "request"
124
+ @logger.warn("received message is not a request") if @logger
125
+ false
126
+ elsif !obj["invocation_id"].is_a?(Integer)
127
+ @logger.warn("invalid invocation id #{obj["invocation_id"]}") if @logger
128
+ false
129
+ else
130
+ true
109
131
  end
110
132
  end
111
133
 
112
- def create_socket
113
- socket = UDPSocket.new
114
- port = PortRangeStart
115
- begin
116
- socket.bind("localhost", port)
117
- rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT
118
- port += 1
119
- retry if port <= PortRangeEnd
120
- raise
134
+ def load_model(sock, request, response)
135
+ problems = @service_provider.get_problems(
136
+ :on_progress => lambda do |frag, work_done, work_overall|
137
+ work_overall = 1 if work_overall < 1
138
+ work_done = work_overall if work_done > work_overall
139
+ work_done = 0 if work_done < 0
140
+ send_response(sock, {
141
+ "type" => "progress",
142
+ "invocation_id" => request["invocation_id"],
143
+ "percentage" => work_done*100/work_overall
144
+ })
145
+ end)
146
+ total = 0
147
+ response["problems"] = problems.collect do |fp|
148
+ { "file" => fp.file,
149
+ "problems" => fp.problems.collect do |p|
150
+ total += 1
151
+ { "severity" => "error", "line" => p.line, "message" => p.message }
152
+ end }
121
153
  end
122
- socket
123
- end
124
-
125
- def refresh(lines)
126
- @service_provider.load_model
127
- []
154
+ response["total_problems"] = total
128
155
  end
129
156
 
130
- def complete(lines)
131
- linepos = lines.shift.to_i
132
- context = ContextBuilder.build_context(@lang, lines, linepos)
133
- @logger.debug("context element: #{@lang.identifier_provider.call(context.element, nil)}") if context && @logger
134
- current_line = lines.pop
135
- current_line ||= ""
136
- options = @completer.complete(context, lambda {|ref|
157
+ def content_complete(sock, request, response)
158
+ # column numbers start at 1
159
+ linepos = request["column"]-1
160
+ lines = request["context"]
161
+ lang = @service_provider.language
162
+ response["options"] = []
163
+ return unless lang
164
+ context = ContextBuilder.build_context(lang, lines, linepos)
165
+ @logger.debug("context element: #{lang.identifier_provider.call(context.element, nil, nil, nil)}") \
166
+ if context && context.element && @logger
167
+ completer = RText::Completer.new(lang)
168
+ options = completer.complete(context, lambda {|ref|
137
169
  @service_provider.get_reference_completion_options(ref, context).collect {|o|
138
170
  Completer::CompletionOption.new(o.identifier, "<#{o.type}>")}
139
171
  })
140
- options.collect { |o|
141
- "#{o.text};#{o.extra}\n"
142
- }
143
- end
144
-
145
- def get_problems(lines, options={})
146
- result = []
147
- severity = options[:with_severity] ? "e;" : ""
148
- @service_provider.get_problems(:on_progress => options[:on_progress]).each do |fp|
149
- result << fp.file+"\n"
150
- fp.problems.each do |p|
151
- result << "#{severity}#{p.line};#{p.message}\n"
152
- end
172
+ response["options"] = options.collect do |o|
173
+ { "insert" => o.text, "display" => "#{o.text} #{o.extra}" }
153
174
  end
154
- result
155
175
  end
156
176
 
157
- def get_reference_targets(lines)
158
- linepos = lines.shift.to_i
159
- current_line = lines.last
160
- context = ContextBuilder.build_context(@lang, lines, lines.last.size)
161
- result = []
162
- if context && current_line[linepos..linepos] =~ /[\w\/]/
163
- ident_start = (current_line.rindex(/[^\w\/]/, linepos) || -1)+1
164
- ident_end = (current_line.index(/[^\w\/]/, linepos) || current_line.size)-1
165
- ident = current_line[ident_start..ident_end]
166
- result << "#{ident_start};#{ident_end}\n"
167
- if current_line[0..linepos+1] =~ /^\s*\w+$/
168
- @service_provider.get_referencing_elements(ident, context).each do |t|
169
- result << "#{t.file};#{t.line};#{t.display_name}\n"
177
+ def link_targets(sock, request, response)
178
+ # column numbers start at 1
179
+ linepos = request["column"]
180
+ lines = request["context"]
181
+ lang = @service_provider.language
182
+ response["targets"] = []
183
+ return unless lang
184
+ link_descriptor = RText::LinkDetector.new(lang).detect(lines, linepos)
185
+ if link_descriptor
186
+ response["begin_column"] = link_descriptor.scol
187
+ response["end_column"] = link_descriptor.ecol
188
+ targets = []
189
+ if link_descriptor.backward
190
+ @service_provider.get_referencing_elements(
191
+ link_descriptor.value, link_descriptor.element, link_descriptor.feature, link_descriptor.index).each do |t|
192
+ targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
170
193
  end
171
194
  else
172
- @service_provider.get_reference_targets(ident, context).each do |t|
173
- result << "#{t.file};#{t.line};#{t.display_name}\n"
195
+ @service_provider.get_reference_targets(
196
+ link_descriptor.value, link_descriptor.element, link_descriptor.feature, link_descriptor.index).each do |t|
197
+ targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
174
198
  end
175
199
  end
200
+ response["targets"] = targets
201
+ end
202
+ end
203
+
204
+ def find_elements(sock, request, response)
205
+ pattern = request["search_pattern"]
206
+ total = 0
207
+ response["elements"] = @service_provider.get_open_element_choices(pattern).collect do |c|
208
+ total += 1
209
+ { "display" => c.display_name, "file" => c.file, "line" => c.line }
210
+ end
211
+ response["total_elements"] = total
212
+ end
213
+
214
+ def send_response(sock, response)
215
+ if response
216
+ begin
217
+ sock.write(serialize_message(response))
218
+ sock.flush
219
+ # if there is an exception, the next read should shutdown the connection properly
220
+ rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
221
+ rescue Exception => e
222
+ # catch Exception to make sure we don't crash due to unexpected exceptions
223
+ @logger.warn "unexpected exception during socket write: #{e.class}"
224
+ end
176
225
  end
177
- result
178
226
  end
179
227
 
180
- def get_open_element_choices(lines)
181
- pattern = lines.shift
182
- @service_provider.get_open_element_choices(pattern).collect do |c|
183
- "#{c.display_name};#{c.file};#{c.line}\n"
228
+ def create_server
229
+ port = PortRangeStart
230
+ serv = nil
231
+ begin
232
+ serv = TCPServer.new("localhost", port)
233
+ if serv.addr[0] == "AF_INET6"
234
+ # we need to make sure that two RText server won't connect on the same port but with IPv4 and IPv6
235
+ # therefore we always use the IPv4 interface, try again to get the IPv4 interface
236
+ # note that using "127.0.0.1" instead of localhost would always get us the IPv4 interface on the
237
+ # first try, however, on some machines using the IP address instead of localhost doesn't work
238
+ # (EACCES error was seen, possibly due to some specific firewall setup)
239
+ serv2 = TCPServer.new("localhost", port)
240
+ serv.close
241
+ serv = serv2
242
+ end
243
+ rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT
244
+ port += 1
245
+ retry if port <= PortRangeEnd
246
+ raise
184
247
  end
248
+ serv
185
249
  end
186
250
 
187
251
  end
@@ -0,0 +1,102 @@
1
+ require 'rtext/generic'
2
+
3
+ module RText
4
+
5
+ module Tokenizer
6
+
7
+ Token = Struct.new(:kind, :value, :line, :scol, :ecol)
8
+
9
+ def tokenize(str, reference_regexp, options={})
10
+ result = []
11
+ on_command_token_proc = options[:on_command_token]
12
+ str.split(/\r?\n/).each_with_index do |str, idx|
13
+ idx += 1
14
+ if str =~ /^\s*([\#@])(.*)/
15
+ if $1 == "#"
16
+ result << Token.new(:comment, $2, idx, str.size-$2.size, str.size)
17
+ else
18
+ result << Token.new(:annotation, $2, idx, str.size-$2.size, str.size)
19
+ end
20
+ else
21
+ col = 1
22
+ first_token_in_line = true
23
+ until str.empty?
24
+ whitespace = false
25
+ case str
26
+ when reference_regexp
27
+ str = $'
28
+ result << Token.new(:reference, $&, idx, col, col+$&.size-1)
29
+ col += $&.size
30
+ when /\A[-+]?\d+\.\d+(?:e[+-]\d+)?\b/
31
+ str = $'
32
+ result << Token.new(:float, $&.to_f, idx, col, col+$&.size-1)
33
+ col += $&.size
34
+ when /\A0[xX][0-9a-fA-F]+\b/
35
+ str = $'
36
+ result << Token.new(:integer, $&.to_i(16), idx, col, col+$&.size-1)
37
+ col += $&.size
38
+ when /\A[-+]?\d+\b/
39
+ str = $'
40
+ result << Token.new(:integer, $&.to_i, idx, col, col+$&.size-1)
41
+ col += $&.size
42
+ when /\A"((?:[^"\\]|\\.)*)"/
43
+ str = $'
44
+ match = $&
45
+ result << Token.new(:string, $1.
46
+ gsub('\\\\','\\').
47
+ gsub('\\"','"').
48
+ gsub('\\n',"\n").
49
+ gsub('\\r',"\r").
50
+ gsub('\\t',"\t").
51
+ gsub('\\f',"\f").
52
+ gsub('\\b',"\b"), idx, col, col+match.size-1)
53
+ col += match.size
54
+ when /\A(?:true|false)\b/
55
+ str = $'
56
+ result << Token.new(:boolean, $& == "true", idx, col, col+$&.size-1)
57
+ col += $&.size
58
+ when /\A([a-zA-Z_]\w*)\b(?:\s*:)?/
59
+ str = $'
60
+ if $&[-1] == ?:
61
+ result << Token.new(:label, $1, idx, col, col+$&.size-1)
62
+ else
63
+ result << Token.new(:identifier, $&, idx, col, col+$&.size-1)
64
+ if first_token_in_line && on_command_token_proc
65
+ on_command_token_proc.call
66
+ end
67
+ end
68
+ col += $&.size
69
+ when /\A[\{\}\[\]:,]/
70
+ str = $'
71
+ result << Token.new($&, nil, idx, col, col+$&.size-1)
72
+ col += $&.size
73
+ when /\A#(.*)/
74
+ str = ""
75
+ result << Token.new(:comment, $1, idx, col, col+$&.size-1)
76
+ when /\A\s+/
77
+ str = $'
78
+ col += $&.size
79
+ whitespace = true
80
+ # ignore
81
+ when /\A<%((?:(?!%>).)*)%>/, /\A<([^>]*)>/
82
+ str = $'
83
+ result << Token.new(:generic, RText::Generic.new($1), idx, col, col+$&.size-1)
84
+ col += $&.size
85
+ when /\A\S+/
86
+ str = $'
87
+ result << Token.new(:error, $&, idx, col, col+$&.size-1)
88
+ col += $&.size
89
+ end
90
+ first_token_in_line = false unless whitespace
91
+ end
92
+ end
93
+ result << Token.new(:newline, nil, idx) \
94
+ unless result.empty? || result.last.kind == :newline
95
+ end
96
+ result
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+