rtext 0.2.1 → 0.3.0

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