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
@@ -13,9 +13,13 @@ class DefaultServiceProvider
13
13
  })
14
14
  end
15
15
 
16
+ def language
17
+ @lang
18
+ end
19
+
16
20
  def load_model(options={})
17
21
  if options[:on_progress]
18
- @loader.load(:after_load => options[:on_progress])
22
+ @loader.load(:on_progress => options[:on_progress])
19
23
  else
20
24
  @loader.load
21
25
  end
@@ -29,8 +33,10 @@ class DefaultServiceProvider
29
33
  clazz = reference.eType.instanceClass
30
34
  targets = @model.index.values.flatten.select{|e| e.is_a?(clazz)}
31
35
  end
36
+ index = 0
32
37
  targets.collect{|t|
33
- ident = @lang.identifier_provider.call(t, context.element)
38
+ ident = @lang.identifier_provider.call(t, context.element, reference, index)
39
+ index += 1
34
40
  if ident
35
41
  ReferenceCompletionOption.new(ident, t.class.ecore.name)
36
42
  else
@@ -40,13 +46,17 @@ class DefaultServiceProvider
40
46
  end
41
47
 
42
48
  ReferenceTarget = Struct.new(:file, :line, :display_name)
43
- def get_reference_targets(identifier, context)
49
+ def get_reference_targets(identifier, element, feature, index)
44
50
  result = []
45
- identifier = @lang.qualify_reference(identifier, context.element)
51
+ urefs = [
52
+ RGen::Instantiator::ReferenceResolver::UnresolvedReference.new(element, feature.name,
53
+ element.getGenericAsArray(feature.name)[index]) ]
54
+ @lang.reference_qualifier.call(urefs, @model)
55
+ identifier = urefs.first.proxy.targetIdentifier
46
56
  targets = @model.index[identifier]
47
57
  if targets && @lang.per_type_identifier
48
- if context.feature
49
- targets = targets.select{|t| t.is_a?(context.feature.eType.instanceClass)}
58
+ if feature
59
+ targets = targets.select{|t| t.is_a?(feature.eType.instanceClass)}
50
60
  end
51
61
  end
52
62
  targets && targets.each do |t|
@@ -58,22 +68,26 @@ class DefaultServiceProvider
58
68
  result
59
69
  end
60
70
 
61
- def get_referencing_elements(identifier, context)
71
+ def get_referencing_elements(identifier, element, feature, index)
62
72
  result = []
63
- targets = @model.index[@lang.identifier_provider.call(context.element, nil)]
73
+ targets = @model.index[@lang.identifier_provider.call(element, nil, nil, nil)]
64
74
  if targets && @lang.per_type_identifier
65
- targets = targets.select{|t| t.class == context.element.class}
75
+ targets = targets.select{|t| t.class == element.class}
66
76
  end
67
77
  if targets && targets.size == 1
68
78
  target = targets.first
69
- elements = target.class.ecore.eAllReferences.select{|r|
70
- r.eOpposite && !r.containment && !r.eOpposite.containment}.collect{|r|
71
- target.getGenericAsArray(r.name)}.flatten
79
+ refs = target.class.ecore.eAllReferences.select{|r|
80
+ r.eOpposite && !r.containment && !r.eOpposite.containment}
81
+ # we only want references configured in the RText language that point to this element
82
+ # thus we don't follow references here which are configured in the language
83
+ # (because for those the other direction is not configured in the language)
84
+ refs -= @lang.non_containments(target.class.ecore)
85
+ elements = refs.collect{|r| target.getGenericAsArray(r.name)}.flatten
72
86
  elements.each do |e|
73
87
  if @lang.fragment_ref(e)
74
88
  path = File.expand_path(@lang.fragment_ref(e).fragment.location)
75
89
  display_name = ""
76
- ident = @lang.identifier_provider.call(e, nil)
90
+ ident = @lang.identifier_provider.call(e, nil, nil, nil)
77
91
  display_name += "#{ident} " if ident
78
92
  display_name += "[#{e.class.ecore.name}]"
79
93
  result << ReferenceTarget.new(path, @lang.line_number(e), display_name)
@@ -148,7 +162,9 @@ class DefaultServiceProvider
148
162
  return @element_name_index if @element_name_index
149
163
  @element_name_index = {}
150
164
  @model.index.each_pair do |ident, elements|
151
- key = ident.split(/\W/).last[0..0].downcase
165
+ last_part = ident.split(/\W/).last
166
+ next unless last_part
167
+ key = last_part[0..0].downcase
152
168
  @element_name_index[key] ||= {}
153
169
  @element_name_index[key][ident] = elements
154
170
  end
@@ -0,0 +1,58 @@
1
+ module RText
2
+ module Frontend
3
+
4
+ module Config
5
+
6
+ def self.find_service_config(file)
7
+ last_dir = nil
8
+ dir = File.expand_path(File.dirname(file))
9
+ search_pattern = file_pattern(file)
10
+ while dir != last_dir
11
+ config_file = "#{dir}/.rtext"
12
+ if File.exist?(config_file)
13
+ configs = parse_config_file(config_file)
14
+ config = configs.find{|s| s.patterns.any?{|p| p == search_pattern}}
15
+ return config if config
16
+ end
17
+ last_dir = dir
18
+ dir = File.dirname(dir)
19
+ end
20
+ nil
21
+ end
22
+
23
+ def self.file_pattern(file)
24
+ ext = File.extname(file)
25
+ if ext.size > 0
26
+ "*#{ext}"
27
+ else
28
+ File.basename(file)
29
+ end
30
+ end
31
+
32
+ ServiceConfig = Struct.new(:file, :patterns, :command)
33
+
34
+ def self.parse_config_file(file)
35
+ configs = []
36
+ File.open(file) do |f|
37
+ lines = f.readlines
38
+ l = lines.shift
39
+ while l
40
+ if l =~ /^(.+):\s*$/
41
+ patterns = $1.split(",").collect{|s| s.strip}
42
+ l = lines.shift
43
+ if l && l =~ /\S/ && l !~ /:\s*$/
44
+ configs << ServiceConfig.new(file, patterns, l)
45
+ l = lines.shift
46
+ end
47
+ else
48
+ l = lines.shift
49
+ end
50
+ end
51
+ end
52
+ configs
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,233 @@
1
+ require 'socket'
2
+ require 'tmpdir'
3
+ require 'rtext/message_helper'
4
+
5
+ module RText
6
+ module Frontend
7
+
8
+ class Connector
9
+ include Process
10
+ include RText::MessageHelper
11
+
12
+ def initialize(config, options={})
13
+ @config = config
14
+ @logger = options[:logger]
15
+ @state = :off
16
+ @invocation_id = 1
17
+ @invocations = {}
18
+ @busy = false
19
+ @busy_start_time = nil
20
+ @connection_listener = options[:connect_callback]
21
+ @outfile_provider = options[:outfile_provider]
22
+ @keep_outfile = options[:keep_outfile]
23
+ @connection_timeout = options[:connection_timeout] || 10
24
+ end
25
+
26
+ def execute_command(obj, options={})
27
+ timeout = options[:timeout] || 5
28
+ @busy = false if @busy_start_time && (Time.now > @busy_start_time + timeout)
29
+ if @busy
30
+ do_work
31
+ :backend_busy
32
+ elsif connected?
33
+ obj["invocation_id"] = @invocation_id
34
+ obj["type"] = "request"
35
+ @socket.send(serialize_message(obj), 0)
36
+ result = nil
37
+ @busy = true
38
+ @busy_start_time = Time.now
39
+ if options[:response_callback]
40
+ @invocations[@invocation_id] = lambda do |r|
41
+ if r["type"] == "response" || r["type"] =~ /error$/
42
+ @busy = false
43
+ end
44
+ options[:response_callback].call(r)
45
+ end
46
+ @invocation_id += 1
47
+ do_work
48
+ :request_pending
49
+ else
50
+ @invocations[@invocation_id] = lambda do |r|
51
+ if r["type"] == "response" || r["type"] =~ /error$/
52
+ result = r
53
+ @busy = false
54
+ end
55
+ end
56
+ @invocation_id += 1
57
+ while !result
58
+ if Time.now > @busy_start_time + timeout
59
+ result = :timeout
60
+ @busy = false
61
+ else
62
+ sleep(0.1)
63
+ do_work
64
+ end
65
+ end
66
+ result
67
+ end
68
+ else
69
+ connect unless connecting?
70
+ do_work
71
+ :connecting
72
+ end
73
+ end
74
+
75
+ def resume
76
+ do_work
77
+ end
78
+
79
+ def stop
80
+ while connecting?
81
+ do_work
82
+ sleep(0.1)
83
+ end
84
+ if connected?
85
+ execute_command({"type" => "request", "command" => "stop"})
86
+ while do_work
87
+ sleep(0.1)
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def connected?
95
+ @state == :connected && backend_running?
96
+ end
97
+
98
+ def connecting?
99
+ @state == :connecting
100
+ end
101
+
102
+ def backend_running?
103
+ if @process_id
104
+ begin
105
+ return true unless waitpid(@process_id, Process::WNOHANG)
106
+ rescue Errno::ECHILD
107
+ end
108
+ end
109
+ false
110
+ end
111
+
112
+ def tempfile_name
113
+ dir = Dir.tmpdir
114
+ i = 0
115
+ file = nil
116
+ while !file || File.exist?(file)
117
+ file = dir+"/rtext.temp.#{i}"
118
+ i += 1
119
+ end
120
+ file
121
+ end
122
+
123
+ def connect
124
+ @state = :connecting
125
+ @connect_start_time = Time.now
126
+
127
+ @logger.info @config.command if @logger
128
+
129
+ if @outfile_provider
130
+ @out_file = @outfile_provider.call
131
+ else
132
+ @out_file = tempfile_name
133
+ end
134
+ File.unlink(@out_file) if File.exist?(@out_file)
135
+
136
+ Dir.chdir(File.dirname(@config.file)) do
137
+ @process_id = spawn(@config.command.strip + " > #{@out_file} 2>&1")
138
+ end
139
+ @work_state = :wait_for_file
140
+ end
141
+
142
+ def do_work
143
+ case @work_state
144
+ when :wait_for_file
145
+ if File.exist?(@out_file)
146
+ @work_state = :wait_for_port
147
+ end
148
+ if Time.now > @connect_start_time + @connection_timeout
149
+ cleanup
150
+ @connection_listener.call(:timeout) if @connection_listener
151
+ @work_state = :done
152
+ @state = :off
153
+ @logger.warn "process didn't startup (connection timeout)" if @logger
154
+ end
155
+ true
156
+ when :wait_for_port
157
+ output = File.read(@out_file)
158
+ if output =~ /^RText service, listening on port (\d+)/
159
+ port = $1.to_i
160
+ @logger.info "connecting to #{port}" if @logger
161
+ begin
162
+ @socket = TCPSocket.new("127.0.0.1", port)
163
+ rescue Errno::ECONNREFUSED
164
+ cleanup
165
+ @connection_listener.call(:timeout) if @connection_listener
166
+ @work_state = :done
167
+ @state = :off
168
+ @logger.warn "could not connect socket (connection timeout)" if @logger
169
+ end
170
+ @state = :connected
171
+ @work_state = :read_from_socket
172
+ @connection_listener.call(:connected) if @connection_listener
173
+ end
174
+ if Time.now > @connect_start_time + @connection_timeout
175
+ cleanup
176
+ @connection_listener.call(:timeout) if @connection_listener
177
+ @work_state = :done
178
+ @state = :off
179
+ @logger.warn "could not connect socket (connection timeout)" if @logger
180
+ end
181
+ true
182
+ when :read_from_socket
183
+ repeat = true
184
+ socket_closed = false
185
+ response_data = ""
186
+ while repeat
187
+ repeat = false
188
+ data = nil
189
+ begin
190
+ data = @socket.read_nonblock(100000)
191
+ rescue Errno::EWOULDBLOCK
192
+ rescue IOError, EOFError, Errno::ECONNRESET
193
+ socket_closed = true
194
+ @logger.info "server socket closed (end of file)" if @logger
195
+ end
196
+ if data
197
+ repeat = true
198
+ response_data.concat(data)
199
+ while obj = extract_message(response_data)
200
+ inv_id = obj["invocation_id"]
201
+ callback = @invocations[inv_id]
202
+ if callback
203
+ callback.call(obj)
204
+ else
205
+ @logger.error "unknown answer" if @logger
206
+ end
207
+ end
208
+ elsif !backend_running? || socket_closed
209
+ cleanup
210
+ @work_state = :done
211
+ return false
212
+ end
213
+ end
214
+ true
215
+ end
216
+
217
+ end
218
+
219
+ def cleanup
220
+ @socket.close if @socket
221
+ # wait up to 2 seconds for backend to shutdown
222
+ for i in 0..20
223
+ break unless backend_running?
224
+ sleep(0.1)
225
+ end
226
+ File.unlink(@out_file) unless @keep_outfile
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+ end
233
+
@@ -0,0 +1,81 @@
1
+ require 'digest'
2
+ require 'rtext/frontend/config'
3
+ require 'rtext/frontend/connector'
4
+
5
+ module RText
6
+ module Frontend
7
+
8
+ class ConnectorManager
9
+
10
+ def initialize(options={})
11
+ @logger = options[:logger]
12
+ @connector_descs = {}
13
+ @connector_listener = options[:connect_callback]
14
+ @keep_outfile = options[:keep_outfile]
15
+ @outfile_provider = options[:outfile_provider]
16
+ @connection_timeout = options[:connection_timeout]
17
+ end
18
+
19
+ ConnectorDesc = Struct.new(:connector, :checksum)
20
+
21
+ def connector_for_file(file)
22
+ config = Config.find_service_config(file)
23
+ if config
24
+ file_pattern = Config.file_pattern(file)
25
+ key = desc_key(config, file_pattern)
26
+ desc = @connector_descs[key]
27
+ if desc
28
+ if desc.checksum == config_checksum(config)
29
+ desc.connector
30
+ else
31
+ # connector must be replaced
32
+ desc.connector.stop
33
+ create_connector(config, file_pattern)
34
+ end
35
+ else
36
+ create_connector(config, file_pattern)
37
+ end
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ def all_connectors
44
+ @connector_descs.values.collect{|v| v.connector}
45
+ end
46
+
47
+ private
48
+
49
+ def create_connector(config, pattern)
50
+ con = Connector.new(config, :logger => @logger, :keep_outfile => @keep_outfile,
51
+ :outfile_provider => @outfile_provider,
52
+ :connection_timeout => @connection_timeout,
53
+ :connect_callback => lambda do |state|
54
+ @connector_listener.call(con, state) if @connector_listener
55
+ end)
56
+ desc = ConnectorDesc.new(con, config_checksum(config))
57
+ key = desc_key(config, pattern)
58
+ @connector_descs[key] = desc
59
+ desc.connector
60
+ end
61
+
62
+ def desc_key(config, pattern)
63
+ config.file.downcase + "," + pattern
64
+ end
65
+
66
+ def config_checksum(config)
67
+ if File.exist?(config.file)
68
+ sha1 = Digest::SHA1.new
69
+ sha1.file(config.file)
70
+ sha1.hexdigest
71
+ else
72
+ nil
73
+ end
74
+ end
75
+
76
+
77
+ end
78
+
79
+ end
80
+ end
81
+