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