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 +10 -2
- data/Rakefile +1 -1
- data/lib/rtext/completer.rb +84 -111
- data/lib/rtext/context_builder.rb +188 -0
- data/lib/rtext/default_loader.rb +8 -0
- data/lib/rtext/default_service_provider.rb +21 -18
- data/lib/rtext/instantiator.rb +28 -9
- data/lib/rtext/language.rb +51 -21
- data/lib/rtext/parser.rb +6 -4
- data/lib/rtext/serializer.rb +3 -2
- data/lib/rtext/service.rb +7 -9
- data/test/completer_test.rb +332 -0
- data/test/context_builder_test.rb +360 -0
- data/test/instantiator_test.rb +95 -13
- data/test/rtext_test.rb +2 -0
- data/test/serializer_test.rb +9 -8
- metadata +8 -11
- data/lib/rtext/context_element_builder.rb +0 -112
data/CHANGELOG
CHANGED
@@ -2,7 +2,15 @@
|
|
2
2
|
|
3
3
|
* First public release
|
4
4
|
|
5
|
-
=0.
|
5
|
+
=0.3.0
|
6
6
|
|
7
|
-
*
|
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
data/lib/rtext/completer.rb
CHANGED
@@ -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(
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
93
|
-
|
94
|
-
|
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
|
+
|
data/lib/rtext/default_loader.rb
CHANGED
@@ -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
|
|