rtext 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+