rtext 0.2.1 → 0.3.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.
data/CHANGELOG CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  * First public release
4
4
 
5
- =0.2.1
5
+ =0.3.0
6
6
 
7
- * Fixed serialization of enum literals starting with a digit
7
+ * Added context sensitive commands
8
+ * Show child role lables in auto completer
9
+ * Show unlabled arguments in auto completer
10
+ * Show only arguments in auto completer which don't have a value yet
11
+ * Fixed auto completion within array values
12
+ * Fixed generation of child role labels in serializer
13
+ * Added :after_load hook to DefaultLoader
14
+ * Added result limit option to DefaultServiceProvider
15
+ * Removed short_class_names option from Language
8
16
 
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ DocFiles = [
8
8
 
9
9
  RTextGemSpec = Gem::Specification.new do |s|
10
10
  s.name = %q{rtext}
11
- s.version = "0.2.1"
11
+ s.version = "0.3.0"
12
12
  s.date = Time.now.strftime("%Y-%m-%d")
13
13
  s.summary = %q{Ruby Textual Modelling}
14
14
  s.email = %q{martin dot thiede at gmx de}
@@ -14,74 +14,99 @@ class Completer
14
14
 
15
15
  # Provides completion options
16
16
  #
17
- # :linestart
18
- # the content of the current line before the cursor
19
- #
20
- # :prev_line_provider
21
- # is a proc which must return lines above the current line
22
- # it receives an index parameter in the range 1..n
23
- # 1 is the line just above the current one, 2 is the second line above, etc.
24
- # the proc must return the line as a string or nil if there is no more line
25
- #
26
17
  # :ref_completion_option_provider
27
18
  # a proc which receives a EReference and should return
28
19
  # the possible completion options as CompletionOption objects
29
20
  # note, that the context element may be nil if this information is unavailable
30
21
  #
31
- def complete(line, linepos, prev_line_provider, ref_completion_option_provider=nil)
32
- linestart = line[0..linepos-1]
33
- # command
34
- if linestart =~ /^\s*(\w*)$/
35
- prefix = $1
36
- classes = completion_classes(prev_line_provider)
37
- classes = classes.select{|c| c.name.index(prefix) == 0} if prefix
38
- classes.sort{|a,b| a.name <=> b.name}.collect do |c|
39
- uargs = @lang.unlabled_arguments(c).collect{|a| "<#{a.name}>"}.join(", ")
40
- CompletionOption.new(c.name, uargs)
41
- end
42
- # attribute
43
- elsif linestart =~ /^\s*(\w+)\s+(?:[^,]+,)*\s*(\w*)$/
44
- command, prefix = $1, $2
45
- clazz = @lang.class_by_command(command)
46
- if clazz
47
- features = @lang.labled_arguments(clazz.ecore)
48
- features = features.select{|f| f.name.index(prefix) == 0} if prefix
49
- features.sort{|a,b| a.name <=> b.name}.collect do |f|
50
- CompletionOption.new("#{f.name}:", "<#{f.eType.name}>")
22
+ def complete(context, ref_completion_option_provider=nil)
23
+ clazz = context && context.element && context.element.class.ecore
24
+ if clazz
25
+ if context.in_block
26
+ types = []
27
+ labled_refs = []
28
+ if context.feature
29
+ if context.feature.is_a?(RGen::ECore::EReference) && context.feature.containment
30
+ types = @lang.concrete_types(context.feature.eType)
31
+ else
32
+ # invalid, ignore
33
+ end
34
+ else
35
+ # all target types which don't need a label
36
+ # and all lables which are needed by a potential target type
37
+ clazz.eAllReferences.select{|r| r.containment}.each do |r|
38
+ ([r.eType] + r.eType.eAllSubTypes).select{|t| !t.abstract}.each do |t|
39
+ if @lang.containments_by_target_type(clazz, t).size > 1
40
+ labled_refs << r
41
+ else
42
+ types << t
43
+ end
44
+ end
45
+ end
51
46
  end
47
+ types.uniq.select{|c| c.name.index(context.prefix) == 0}.
48
+ sort{|a,b| a.name <=> b.name}.collect do |c|
49
+ class_completion_option(c)
50
+ end +
51
+ labled_refs.uniq.select{|r| r.name.index(context.prefix) == 0}.
52
+ sort!{|a,b| a.name <=> b.name}.collect do |r|
53
+ CompletionOption.new("#{r.name}:", "<#{r.eType.name}>")
54
+ end
52
55
  else
53
- []
54
- end
55
- # value
56
- elsif linestart =~ /\s*(\w+)\s+(?:[^,]+,)*\s*(\w+):\s*(\S*)$/
57
- command, fn, prefix = $1, $2, $3
58
- clazz = @lang.class_by_command(command)
59
- feature = clazz && @lang.non_containments(clazz.ecore).find{|f| f.name == fn}
60
- if feature
61
- if feature.is_a?(RGen::ECore::EReference)
62
- if ref_completion_option_provider
63
- ref_completion_option_provider.call(feature)
56
+ if context.feature
57
+ # value completion
58
+ if context.feature.is_a?(RGen::ECore::EAttribute) || !context.feature.containment
59
+ if context.feature.is_a?(RGen::ECore::EReference)
60
+ if ref_completion_option_provider
61
+ ref_completion_option_provider.call(context.feature)
62
+ else
63
+ []
64
+ end
65
+ elsif context.feature.eType.is_a?(RGen::ECore::EEnum)
66
+ context.feature.eType.eLiterals.collect do |l|
67
+ CompletionOption.new("#{l.name}")
68
+ end
69
+ elsif context.feature.eType.instanceClass == String
70
+ [ CompletionOption.new("\"\"") ]
71
+ elsif context.feature.eType.instanceClass == Integer
72
+ (0..4).collect{|i| CompletionOption.new("#{i}") }
73
+ elsif context.feature.eType.instanceClass == Float
74
+ (0..4).collect{|i| CompletionOption.new("#{i}.0") }
75
+ elsif context.feature.eType.instanceClass == RGen::MetamodelBuilder::DataTypes::Boolean
76
+ [true, false].collect{|b| CompletionOption.new("#{b}") }
77
+ else
78
+ []
79
+ end
64
80
  else
65
- []
81
+ # containment reference, ignore
66
82
  end
67
- elsif feature.eType.is_a?(RGen::ECore::EEnum)
68
- feature.eType.eLiterals.collect do |l|
69
- CompletionOption.new("#{l.name}")
70
- end
71
- elsif feature.eType.instanceClass == String
72
- [ CompletionOption.new("\"\"") ]
73
- elsif feature.eType.instanceClass == Integer
74
- (0..4).collect{|i| CompletionOption.new("#{i}") }
75
- elsif feature.eType.instanceClass == Float
76
- (0..4).collect{|i| CompletionOption.new("#{i}.0") }
77
- elsif feature.eType.instanceClass == RGen::MetamodelBuilder::DataTypes::Boolean
78
- [true, false].collect{|b| CompletionOption.new("#{b}") }
79
83
  else
80
- []
84
+ result = []
85
+ if !@lang.labled_arguments(clazz).any?{|f|
86
+ context.element.getGenericAsArray(f.name).size > 0}
87
+ result += @lang.unlabled_arguments(clazz).
88
+ select{|f| f.name.index(context.prefix) == 0 &&
89
+ context.element.getGenericAsArray(f.name).empty?}[0..0].
90
+ sort{|a,b| a.name <=> b.name}.collect do |f|
91
+ CompletionOption.new("<#{f.name}>", "<#{f.eType.name}>")
92
+ end
93
+ end
94
+ # label completion
95
+ result += @lang.labled_arguments(clazz).
96
+ select{|f| f.name.index(context.prefix) == 0 &&
97
+ context.element.getGenericAsArray(f.name).empty?}.
98
+ sort{|a,b| a.name <=> b.name}.collect do |f|
99
+ CompletionOption.new("#{f.name}:", "<#{f.eType.name}>")
100
+ end
101
+ result
81
102
  end
82
- else
83
- []
84
103
  end
104
+ elsif context
105
+ # root classes
106
+ @lang.root_classes.select{|c| c.name.index(context.prefix) == 0}.
107
+ sort{|a,b| a.name <=> b.name}.collect do |c|
108
+ class_completion_option(c)
109
+ end
85
110
  else
86
111
  []
87
112
  end
@@ -89,61 +114,9 @@ class Completer
89
114
 
90
115
  private
91
116
 
92
- def completion_classes(prev_line_provider)
93
- clazz, feature = context(prev_line_provider)
94
- if clazz
95
- if feature
96
- @lang.concrete_types(feature.eType)
97
- else
98
- refs_by_class = {}
99
- clazz.eAllReferences.select{|r| r.containment}.each do |r|
100
- @lang.concrete_types(r.eType).each { |c| (refs_by_class[c] ||= []) << r }
101
- end
102
- refs_by_class.keys.select{|c| refs_by_class[c].size == 1}
103
- end
104
- else
105
- @lang.root_epackage.eAllClasses.select{|c| !c.abstract &&
106
- !c.eAllReferences.any?{|r| r.eOpposite && r.eOpposite.containment}}
107
- end
108
- end
109
-
110
- def context(prev_line_provider)
111
- command, role = parse_context(prev_line_provider)
112
- clazz = command && @lang.root_epackage.eAllClasses.find{|c| c.name == command}
113
- feature = role && clazz && clazz.eAllReferences.find{|r| r.containment && r.name == role}
114
- [clazz, feature]
115
- end
116
-
117
- def parse_context(prev_line_provider)
118
- block_nesting = 0
119
- array_nesting = 0
120
- non_empty_lines = 0
121
- role = nil
122
- i = 0
123
- while line = prev_line_provider.call(i+=1)
124
- # empty or comment
125
- next if line =~ /^\s*$/ || line =~ /^\s*#/
126
- # role
127
- if line =~ /^\s*(\w+):\s*$/
128
- role = $1 if non_empty_lines == 0
129
- # block open
130
- elsif line =~ /^\s*(\S+).*\{\s*$/
131
- block_nesting -= 1
132
- return [$1, role] if block_nesting < 0
133
- # block close
134
- elsif line =~ /^\s*\}\s*$/
135
- block_nesting += 1
136
- # array open
137
- elsif line =~ /^\s*(\w+):\s*\[\s*$/
138
- array_nesting -= 1
139
- role = $1 if array_nesting < 0
140
- # array close
141
- elsif line =~ /^\s*\]\s*$/
142
- array_nesting += 1
143
- end
144
- non_empty_lines += 1
145
- end
146
- [nil, nil]
117
+ def class_completion_option(eclass)
118
+ uargs = @lang.unlabled_arguments(eclass).collect{|a| "<#{a.name}>"}.join(", ")
119
+ CompletionOption.new(@lang.command_by_class(eclass.instanceClass), uargs)
147
120
  end
148
121
 
149
122
  end
@@ -0,0 +1,188 @@
1
+ require 'rtext/instantiator'
2
+
3
+ module RText
4
+
5
+ # The ContextBuilder builds context information for a set of context lines and the
6
+ # cursor position in the current line. The context consists of
7
+ #
8
+ # * the context element, i.e. the element surrounding the current cursor position,
9
+ # the element is a new stand-alone element with all parent elements set up to the root,
10
+ # all attributes and non-containment references before the cursor position will be set,
11
+ # values right or below of the current cursor position will be ommitted,
12
+ # the value directly left of the cursor with no space in between will also be ommitted,
13
+ # (it is assumed that the value is currently being completed)
14
+ # references are not being resolved
15
+ #
16
+ # * the current feature or nil if it can not be determined
17
+ # if the cursor is inside or directly behind a role label, this label will be ignored
18
+ # (it is assumed that the lable is currently being completed)
19
+ #
20
+ # * the completion prefix, this is the word directly left of the cursor
21
+ #
22
+ # * flag if cursor is in an array (i.e. within square brackets)
23
+ #
24
+ # * flag if the cursor is in the content block of the context element (i.e. within curly braces)
25
+ #
26
+ module ContextBuilder
27
+
28
+ Context = Struct.new(:element, :feature, :prefix, :in_array, :in_block)
29
+
30
+ class << self
31
+
32
+ # Builds the context information based on a set of +content_lines+. Content lines
33
+ # are the RText lines containing the nested command headers in the original order.
34
+ # The cursor is assumed to be in the last context line at column +position_in_line+
35
+ def build_context(language, context_lines, position_in_line)
36
+ context_info = fix_context(context_lines, position_in_line)
37
+ return nil unless context_info
38
+ element = instantiate_context_element(language, context_info)
39
+ if element
40
+ feature = context_info.role &&
41
+ element.class.ecore.eAllStructuralFeatures.find{|f| f.name == context_info.role}
42
+ Context.new(element, feature, context_info.prefix, context_info.in_array, context_info.in_block)
43
+ else
44
+ Context.new(nil, nil, context_info.prefix, context_info.in_array, context_info.in_block)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def instantiate_context_element(language, context_info)
51
+ root_elements = []
52
+ problems = []
53
+ text = context_info.lines.join("\n")
54
+ Instantiator.new(language).instantiate(text,
55
+ :root_elements => root_elements, :problems => problems)
56
+ if root_elements.size > 0
57
+ find_leaf_child(root_elements.first, context_info.num_elements-1)
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ def find_leaf_child(element, num_required_children)
64
+ childs = element.class.ecore.eAllReferences.select{|r| r.containment}.collect{|r|
65
+ element.getGenericAsArray(r.name)}.flatten
66
+ if childs.size > 0
67
+ find_leaf_child(childs.first, num_required_children-1)
68
+ elsif num_required_children == 0
69
+ element
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ ContextInternal = Struct.new(:lines, :num_elements, :role, :prefix, :in_array, :in_block)
76
+
77
+ # extend +context_lines+ into a set of lines which can be processed by the RText
78
+ def fix_context(context_lines, position_in_line)
79
+ context_lines = context_lines.dup
80
+ # make sure there is at least one line
81
+ # the frontent may ommit the last context line if the cursor is at collumn 0
82
+ if context_lines.empty? || (position_in_line == 0 && context_lines.last != "")
83
+ context_lines << ""
84
+ end
85
+ position_in_line ||= context_lines.last.size
86
+ # cut off last line right of cursor
87
+ context_lines << context_lines.pop[0..position_in_line-1]
88
+ line = context_lines.last
89
+ if line =~ /^\s*\w+\s+/
90
+ # this line contains a new element
91
+ num_elements = 1
92
+ in_block = false
93
+ # labled array value
94
+ if line =~ /\W(\w+):\s*\[([^\]]*)$/
95
+ role = $1
96
+ array_content = $2
97
+ in_array = true
98
+ if array_content =~ /,\s*(\S*)$/
99
+ prefix = $1
100
+ line.sub!(/,\s*\S*$/, "]")
101
+ else
102
+ array_content =~ /\s*(\S*)$/
103
+ prefix = $1
104
+ line.sub!(/\[[^\]]*$/, "[]")
105
+ end
106
+ # labled value
107
+ elsif line =~ /\W(\w+):\s*(\S*)$/
108
+ role = $1
109
+ prefix = $2
110
+ in_array = false
111
+ line.sub!(/\s*\w+:\s*\S*$/, "")
112
+ line.sub!(/,$/, "")
113
+ # unlabled value or label
114
+ elsif line =~ /[,\s](\S*)$/
115
+ role = nil
116
+ prefix = $1
117
+ in_array = false
118
+ line.sub!(/\s*\S*$/, "")
119
+ line.sub!(/,$/, "")
120
+ # TODO: unlabled array value
121
+ else
122
+ # parse problem
123
+ return nil
124
+ end
125
+ else
126
+ # this line is in the content block
127
+ num_elements = 0
128
+ in_block = true
129
+ # role or new element
130
+ if line =~ /^\s*(\w*)$/
131
+ prefix = $1
132
+ role, in_array = find_role(context_lines[0..-2])
133
+ # fix single role lable
134
+ if context_lines[-2] =~ /^\s*\w+:\s*$/
135
+ context_lines[-1] = context_lines.pop
136
+ end
137
+ else
138
+ # comment, closing brackets, etc.
139
+ return nil
140
+ end
141
+ end
142
+ context_lines.reverse.each do |l|
143
+ if l =~ /\{\s*$/
144
+ context_lines << "}"
145
+ num_elements += 1
146
+ elsif l =~ /\[\s*$/
147
+ context_lines << "]"
148
+ end
149
+ end
150
+ ContextInternal.new(context_lines, num_elements, role, prefix, in_array, in_block)
151
+ end
152
+
153
+ def find_role(context_lines)
154
+ block_nesting = 0
155
+ array_nesting = 0
156
+ non_empty_lines = 0
157
+ context_lines.reverse.each do |line|
158
+ # empty or comment
159
+ next if line =~ /^\s*$/ || line =~ /^\s*#/
160
+ # role
161
+ if line =~ /^\s*(\w+):\s*$/
162
+ return [$1, false] if non_empty_lines == 0
163
+ # block open
164
+ elsif line =~ /^\s*(\S+).*\{\s*$/
165
+ block_nesting -= 1
166
+ return [nil, false] if block_nesting < 0
167
+ # block close
168
+ elsif line =~ /^\s*\}\s*$/
169
+ block_nesting += 1
170
+ # array open
171
+ elsif line =~ /^\s*(\w+):\s*\[\s*$/
172
+ array_nesting -= 1
173
+ return [$1, true] if array_nesting < 0
174
+ # array close
175
+ elsif line =~ /^\s*\]\s*$/
176
+ array_nesting += 1
177
+ end
178
+ non_empty_lines += 1
179
+ end
180
+ [nil, false]
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
@@ -50,8 +50,13 @@ class DefaultLoader
50
50
  # into and a symbol indicating the kind of loading: :load, :load_cached, :load_update_cache
51
51
  # default: no before load proc
52
52
  #
53
+ # :after_load
54
+ # a proc which is called after a file has been loaded, receives the fragment loaded
55
+ # default: no after load proc
56
+ #
53
57
  def load(options={})
54
58
  @before_load_proc = options[:before_load]
59
+ @after_load_proc = options[:after_load]
55
60
  files = @file_provider.call
56
61
  @change_detector.check_files(files)
57
62
  @model.resolve(:fragment_provider => method(:fragment_provider),
@@ -99,12 +104,15 @@ class DefaultLoader
99
104
  @before_load_proc && @before_load_proc.call(fragment, :load_update_cache)
100
105
  load_fragment(fragment)
101
106
  @cache.store(fragment)
107
+ @after_load_proc && @after_load_proc.call(fragment)
102
108
  else
103
109
  @before_load_proc && @before_load_proc.call(fragment, :load_cached)
110
+ @after_load_proc && @after_load_proc.call(fragment)
104
111
  end
105
112
  else
106
113
  @before_load_proc && @before_load_proc.call(fragment, :load)
107
114
  load_fragment(fragment)
115
+ @after_load_proc && @after_load_proc.call(fragment)
108
116
  end
109
117
  end
110
118