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
@@ -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
+