rtext 0.7.0 → 0.8.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dfdb25504469a30431bda0d53f6ed450809ca7ba
4
+ data.tar.gz: 93de80eedec085dfb25d6fb1d5f134236f692c57
5
+ SHA512:
6
+ metadata.gz: dfe6ce9af736e941413be2ec99c4ad301ded2192affc6334dcf80eb795b2b9a98370b1174c385f852f0ba0788e9d2cf6db1130955eda6e845eca4758f5580e8d
7
+ data.tar.gz: 80f81b162444e33c346f5edacd961828c2311c9d99f1f404a94a62cac40a9f2dd6403608e7555da8f60210918f102cc92d308861dcf06ce871cdc2b0b00be79b
data/CHANGELOG CHANGED
@@ -76,3 +76,7 @@
76
76
  * Minor performance improvements, mainly for instantiator and when passing large RText messages
77
77
  * Improved performance of frontend connector especially for large amounts of data
78
78
 
79
+ =0.8.0
80
+
81
+ * Added line breaks support
82
+
@@ -493,3 +493,11 @@ context in the frontend simple and the amount of data transmitted to the backend
493
493
  It's also a way to keep the parsing time of the context low in the backend and thus to minimize
494
494
  the user noticable delay.
495
495
 
496
+ In case of line breaks, the frontend is responsible to join the lines before sending the
497
+ context information. For commands which use a column position, the position is the position
498
+ within the joined line. This means that, when sending a command, the frontend must convert
499
+ the column position in the broken line into the new position in the joined line.
500
+ When reading back column information in a response (e.g. link command) the frontend must
501
+ convert the column position in the joined line into a position in the respective broken
502
+ fragment of a line.
503
+
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.7.0"
11
+ s.version = "0.8.0.pre1"
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}
@@ -103,8 +103,10 @@ module ContextBuilder
103
103
  problem = nil
104
104
  line = context_lines.last
105
105
  if line =~ /\{\s*$/
106
- # remove curly brace from last line, required for correct counting of num_elements
107
- line.sub!(/\{\s*$/,"")
106
+ # remove curly brace from last line, required for correct counting of num_elements;
107
+ # also make sure that there is whitespace at the end of line, otherwise a word
108
+ # might get removed as "just being completed"
109
+ line.sub!(/\{\s*$/," ")
108
110
  problem = :after_curly
109
111
  end
110
112
 
@@ -1,10 +1,14 @@
1
1
  module RText
2
2
  module Frontend
3
3
 
4
- module Context
4
+ class Context
5
5
 
6
- # lines; all lines from the beginning up to and including the current line
7
- def self.extract(lines)
6
+ # lines: all lines from the beginning up to and including the current line
7
+ # pos: position of the cursor in the last lines
8
+ # returns the extracted lines and the new position in the last line
9
+ def extract(lines, pos)
10
+ lines = filter_lines(lines)
11
+ lines, new_pos = join_lines(lines, pos)
8
12
  non_ignored_lines = 0
9
13
  array_nesting = 0
10
14
  block_nesting = 0
@@ -14,39 +18,67 @@ def self.extract(lines)
14
18
  if i == 0
15
19
  result.unshift(l)
16
20
  else
17
- l = l.strip
18
- if l.size == 0 || l[0..0] == "#"
19
- # ignore empty lines and comments
20
- else
21
- non_ignored_lines += 1
22
- case l[-1..-1]
23
- when "{"
24
- if block_nesting > 0
25
- block_nesting -= 1
26
- elsif block_nesting == 0
27
- result.unshift(l)
28
- last_element_line = non_ignored_lines
29
- end
30
- when "}"
31
- block_nesting += 1
32
- when "["
33
- if array_nesting > 0
34
- array_nesting -= 1
35
- elsif array_nesting == 0
36
- result.unshift(l)
37
- end
38
- when "]"
39
- array_nesting += 1
40
- when ":"
41
- # lable directly above element
42
- if non_ignored_lines == last_element_line + 1
43
- result.unshift(l)
44
- end
21
+ non_ignored_lines += 1
22
+ case l.strip[-1..-1]
23
+ when "{"
24
+ if block_nesting > 0
25
+ block_nesting -= 1
26
+ elsif block_nesting == 0
27
+ result.unshift(l)
28
+ last_element_line = non_ignored_lines
45
29
  end
30
+ when "}"
31
+ block_nesting += 1
32
+ when "["
33
+ if array_nesting > 0
34
+ array_nesting -= 1
35
+ elsif array_nesting == 0
36
+ result.unshift(l)
37
+ end
38
+ when "]"
39
+ array_nesting += 1
40
+ when ":"
41
+ # lable directly above element
42
+ if non_ignored_lines == last_element_line + 1
43
+ result.unshift(l)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ [result, new_pos]
49
+ end
50
+
51
+ def filter_lines(lines)
52
+ lines.reject { |l|
53
+ ls = l.strip
54
+ ls[0..0] == "@" || ls[0..0] == "#"
55
+ }
56
+ end
57
+
58
+ # when joining two lines, all whitespace after the last character of the first line is removed
59
+ # (after , and [); however whitespace at the end of the last of several joined lines is preserved;
60
+ # this way the context is correct even if the cursor is after the end of the last line
61
+ # (i.e. with whitespace after the last non-whitespace character)
62
+ def join_lines(lines, pos)
63
+ outlines = []
64
+ while lines.size > 0
65
+ outlines << lines.shift
66
+ while lines.size > 0 &&
67
+ (outlines.last =~ /,\s*$/ ||
68
+ (outlines.last =~ /\[\s*$/ && outlines.last =~ /,/) ||
69
+ (lines.first =~ /^\s*\]/ && outlines.last =~ /,/))
70
+ outlines.last.rstrip!
71
+ l = lines.shift
72
+ if lines.size == 0
73
+ # strip only left part, the prefix might have whitespace on the
74
+ # right hand side which is relevant for the position
75
+ non_ws_prefix = l[0..pos-1].lstrip
76
+ pos = outlines.last.size + non_ws_prefix.size
46
77
  end
78
+ outlines.last.concat(l.lstrip)
47
79
  end
48
80
  end
49
- result
81
+ [outlines, pos]
50
82
  end
51
83
 
52
84
  end
@@ -42,6 +42,16 @@ class Language
42
42
  # (in sprintf syntax) which will be used by the serializer for integers and floats.
43
43
  # default: if not present or the proc returns nil, then #to_s is used
44
44
  #
45
+ # :newline_arguments
46
+ # a Proc which receives an EClass and should return the names of this EClass's features
47
+ # which are to be serialized after adding a line break.
48
+ # default: no attributes on new lines
49
+ #
50
+ # :newline_arrays
51
+ # a Proc which receives an EClass and should return the names of this EClass's features
52
+ # for which the array elements of their values are to be serialized after a line break.
53
+ # default: no attributes on new lines
54
+ #
45
55
  # :reference_regexp
46
56
  # a Regexp which is used by the tokenizer for identifying references
47
57
  # it must only match at the beginning of a string, i.e. it should start with \A
@@ -53,20 +63,25 @@ class Language
53
63
  # :identifier_provider
54
64
  # a Proc which receives an element, its containing element, the feature through which the
55
65
  # element is referenced and the index position of the reference within the feature's values.
56
- # the latter 3 argumnets may be nil. it should return the element's identifier as a string.
57
- # the identifier must be unique for the element unless "per_type_identifier" is set to true,
66
+ # the latter 3 arguments may be nil. it should return the element's identifier as a string.
67
+ # the identifier must be unique for the element unless :per_type_identifier is set to true,
58
68
  # in which case they must be unique for each element of the same type.
59
69
  # identifiers may be relative to the given containing element, depending on the given
60
70
  # feature and index position. in this case a globally unique
61
- # identifier must be resonstructed by the proc specified using the :reference_qualifier option.
71
+ # identifier must be reconstructed by the proc specified using the :reference_qualifier option.
62
72
  # if the containing element is nil, the identifier returned must be globally unique.
63
- # default: identifiers calculated by QualifiedNameProvider
73
+ # default: identifiers calculated by QualifiedNameProvider using the attribute provided using
74
+ # the :attribute_name option
64
75
  # in this case options to QualifiedNameProvider may be provided and will be passed through
65
76
  #
66
77
  # :per_type_identifier
67
78
  # if set to true, identifiers may be reused for elements of different type
68
79
  # default: false
69
80
  #
81
+ # :attribute_name
82
+ # the name of the attribute used to calculate identifiers by the default identifier provider
83
+ # default: "name"
84
+ #
70
85
  # :reference_qualifier
71
86
  # a Proc which receives RGen unresolved references and either a FragmentedModel or a ModelFragment.
72
87
  # it should modify the unresolved references' targetIdentifiers to make them globally unique.
@@ -142,6 +157,8 @@ class Language
142
157
  @unquoted_arguments = options[:unquoted_arguments]
143
158
  @labeled_containments = options[:labeled_containments]
144
159
  @argument_format_provider = options[:argument_format_provider]
160
+ @newline_arguments = options[:newline_arguments]
161
+ @newline_arrays = options[:newline_arrays]
145
162
  @root_classes = options[:root_classes] || default_root_classes(root_epackage)
146
163
  command_name_provider = options[:command_name_provider] || proc{|c| c.name}
147
164
  setup_commands(root_epackage, command_name_provider)
@@ -232,6 +249,16 @@ class Language
232
249
  @argument_format_provider && @argument_format_provider.call(feature)
233
250
  end
234
251
 
252
+ def newline_argument?(clazz, feature)
253
+ return false unless @newline_arguments
254
+ @newline_arguments.call(clazz).include?(feature.name)
255
+ end
256
+
257
+ def newline_array?(clazz, feature)
258
+ return false unless @newline_arrays
259
+ @newline_arrays.call(clazz).include?(feature.name)
260
+ end
261
+
235
262
  def concrete_types(clazz)
236
263
  ([clazz] + clazz.eAllSubTypes).select{|c| !c.abstract}
237
264
  end
@@ -312,6 +339,8 @@ class Language
312
339
  :labled_arguments,
313
340
  :unquoted?,
314
341
  :labeled_containment?,
342
+ :newline_argument?,
343
+ :newline_array?,
315
344
  :argument_format,
316
345
  :concrete_types,
317
346
  :containments_by_target_type,
@@ -130,7 +130,10 @@ class Parser
130
130
  def parse_argument_list(arg_list)
131
131
  first = true
132
132
  while (AnyValue + [",", "[", :label, :error]).include?(next_token_kind)
133
- consume(",") unless first
133
+ unless first
134
+ success = consume(",")
135
+ consume(:newline) if success && next_token_kind == :newline
136
+ end
134
137
  first = false
135
138
  parse_argument(arg_list)
136
139
  end
@@ -155,13 +158,18 @@ class Parser
155
158
 
156
159
  def parse_argument_value_list
157
160
  consume("[")
161
+ consume(:newline) if next_token_kind == :newline
158
162
  first = true
159
163
  result = []
160
164
  while (AnyValue + [",", :error]).include?(next_token_kind)
161
- consume(",") unless first
165
+ unless first
166
+ success = consume(",")
167
+ consume(:newline) if success && next_token_kind == :newline
168
+ end
162
169
  first = false
163
170
  result << parse_value
164
171
  end
172
+ consume(:newline) if next_token_kind == :newline && next_token_kind(1) == "]"
165
173
  consume("]")
166
174
  result
167
175
  end
@@ -172,8 +180,8 @@ class Parser
172
180
  consume(*AnyValue)
173
181
  end
174
182
 
175
- def next_token_kind
176
- @tokens.first && @tokens.first.kind
183
+ def next_token_kind(idx=0)
184
+ @tokens[idx] && @tokens[idx].kind
177
185
  end
178
186
 
179
187
  def discard_until(kind)
@@ -72,13 +72,24 @@ class Serializer
72
72
  args = []
73
73
  @lang.unlabled_arguments(clazz).each do |f|
74
74
  values = serialize_values(element, f)
75
- args << values if values
75
+ args << [f, values] if values
76
76
  end
77
77
  @lang.labled_arguments(clazz).each do |f|
78
78
  values = serialize_values(element, f)
79
- args << "#{f.name}: #{values}" if values
79
+ args << [f, "#{f.name}: #{values}"] if values
80
+ end
81
+ newline_arguments = false
82
+ args.each_with_index do |arg, index|
83
+ if @lang.newline_argument?(clazz, arg[0])
84
+ headline += " \\" if index == 0
85
+ headline += "\n" + @lang.indent_string * (@indent + 1)
86
+ newline_arguments = true
87
+ else
88
+ headline += " "
89
+ end
90
+ headline += arg[1]
91
+ headline += "," unless index == args.size-1
80
92
  end
81
- headline += " "+args.join(", ") if args.size > 0
82
93
  contained_elements = {}
83
94
  @lang.containments(clazz).each do |f|
84
95
  contained_elements[f] = element.getGenericAsArray(f.name)
@@ -87,6 +98,10 @@ class Serializer
87
98
  headline += " {"
88
99
  write(headline)
89
100
  iinc
101
+ # additional indentation needed if there are arguments on separate lines;
102
+ # note that this increment doesn't affect indentation of features of this element
103
+ # that have array values, because they have already been formatted in serialize_values
104
+ iinc if newline_arguments
90
105
  @lang.containments(clazz).each do |f|
91
106
  childs = contained_elements[f]
92
107
  if childs.size > 0
@@ -111,6 +126,7 @@ class Serializer
111
126
  end
112
127
  end
113
128
  idec
129
+ idec if newline_arguments
114
130
  write("}")
115
131
  else
116
132
  write(headline)
@@ -173,10 +189,17 @@ class Serializer
173
189
  result << @lang.identifier_provider.call(v, element, feature, index)
174
190
  end
175
191
  end
176
- if result.size > 1
177
- "[#{result.join(", ")}]"
192
+ if result.size > 1
193
+ if @lang.newline_array?(element.class.ecore, feature)
194
+ # inside an array, indent two steps further than the command
195
+ "[\n" + @lang.indent_string * (@indent + 2) +
196
+ result.join(",\n" + @lang.indent_string * (@indent + 2)) +
197
+ "\n" + @lang.indent_string * (@indent + 1) + "]"
198
+ else
199
+ "[#{result.join(", ")}]"
200
+ end
178
201
  elsif result.size == 1
179
- result.first
202
+ result.first
180
203
  else
181
204
  nil
182
205
  end
@@ -10,6 +10,7 @@ module Tokenizer
10
10
  def tokenize(str, reference_regexp, options={})
11
11
  result = []
12
12
  on_command_token_proc = options[:on_command_token]
13
+ linebreak = false
13
14
  str.split(/\r?\n/).each_with_index do |str, idx|
14
15
  idx += 1
15
16
  if idx == 1
@@ -27,7 +28,12 @@ module Tokenizer
27
28
  end
28
29
  else
29
30
  col = 1
30
- first_token_in_line = true
31
+ if linebreak
32
+ # do not regard as the first token, if previous line ended in a linebreak
33
+ linebreak = false
34
+ else
35
+ first_token_in_line = true
36
+ end
31
37
  until str.empty?
32
38
  whitespace = false
33
39
  case str
@@ -94,6 +100,10 @@ module Tokenizer
94
100
  col += $&.size
95
101
  whitespace = true
96
102
  # ignore
103
+ when /\A\\\s*\Z/
104
+ str = $'
105
+ linebreak = true
106
+ # ignore
97
107
  when /\A<%((?:(?!%>).)*)%>/, /\A<([^>]*)>/
98
108
  str = $'
99
109
  result << Token.new(:generic, RText::Generic.new($1), idx, col, col+$&.size-1)
@@ -107,7 +117,7 @@ module Tokenizer
107
117
  end
108
118
  end
109
119
  result << Token.new(:newline, nil, idx) \
110
- unless result.empty? || result.last.kind == :newline
120
+ unless linebreak || result.empty? || result.last.kind == :newline
111
121
  end
112
122
  result
113
123
  end
@@ -397,6 +397,14 @@ TestNode {|
397
397
  assert(c.element.is_a?(TestMM::TestNode))
398
398
  end
399
399
 
400
+ def test_root_after_curly_no_ws
401
+ c = build_context TestMM, <<-END
402
+ TestNode{|
403
+ END
404
+ assert_context c, :prefix => "", :feature => nil, :in_array => false, :in_block => false, :problem => :after_curly
405
+ assert(c.element.is_a?(TestMM::TestNode))
406
+ end
407
+
400
408
  def test_in_cmd_after_cmd
401
409
  c = build_context TestMM, <<-END
402
410
  TestNode text: a {
@@ -0,0 +1,205 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),"..","..","lib")
2
+
3
+ require 'test/unit'
4
+ require 'rtext/frontend/context'
5
+
6
+ class ContextTest < Test::Unit::TestCase
7
+
8
+ def test_simple
9
+ assert_context(
10
+ %Q(
11
+ A {
12
+ B {
13
+ |F bla
14
+ ),
15
+ %Q(
16
+ A {
17
+ B {
18
+ C a1: v1, a2: "v2"
19
+ D {
20
+ E a1: 5
21
+ }
22
+ |F bla
23
+ ))
24
+ end
25
+
26
+ def test_child_label
27
+ assert_context(
28
+ %Q(
29
+ A {
30
+ sub:
31
+ B {
32
+ F bla|
33
+ ),
34
+ %Q(
35
+ A {
36
+ sub:
37
+ B {
38
+ C a1: v1, a2: "v2"
39
+ D {
40
+ E a1: 5
41
+ }
42
+ F bla|
43
+ ))
44
+ end
45
+
46
+ def test_child_label_array
47
+ assert_context(
48
+ %Q(
49
+ A {
50
+ sub: [
51
+ B {
52
+ F| bla
53
+ ),
54
+ %Q(
55
+ A {
56
+ sub: [
57
+ B {
58
+ C
59
+ }
60
+ B {
61
+ C a1: v1, a2: "v2"
62
+ D {
63
+ E a1: 5
64
+ }
65
+ F| bla
66
+ ))
67
+ end
68
+
69
+ def test_ignore_child_lables
70
+ assert_context(
71
+ %Q(
72
+ A {
73
+ B {
74
+ F bl|a
75
+ ),
76
+ %Q(
77
+ A {
78
+ B {
79
+ sub:
80
+ C a1: v1, a2: "v2"
81
+ sub2: [
82
+ D {
83
+ E a1: 5
84
+ }
85
+ ]
86
+ F bl|a
87
+ ))
88
+ end
89
+
90
+ def test_linebreak
91
+ assert_context(
92
+ %Q(
93
+ A {
94
+ B {
95
+ C name,a1: v1,a2: "v2"|
96
+ ),
97
+ %Q(
98
+ A {
99
+ B {
100
+ C name,
101
+ a1: v1,
102
+ a2: "v2"|
103
+ ))
104
+ end
105
+
106
+ def test_linebreak_arg_array
107
+ assert_context(
108
+ %Q(
109
+ A {
110
+ B {
111
+ C name,a1: [v1,v2],a2: |5
112
+ ),
113
+ %Q(
114
+ A {
115
+ B {
116
+ C name,
117
+ a1: [
118
+ v1,
119
+ v2
120
+ ],
121
+ a2: |5
122
+ ))
123
+ end
124
+
125
+ def test_linebreak_empty_last_line
126
+ assert_context(
127
+ %Q(
128
+ A {
129
+ B name,|
130
+ ),
131
+ %Q(
132
+ A {
133
+ B name,
134
+ |
135
+ ))
136
+ end
137
+
138
+ def test_linebreak_empty_last_line2
139
+ assert_context(
140
+ %Q(
141
+ A {
142
+ B name,|
143
+ ),
144
+ %Q(
145
+ A {
146
+ B name,
147
+ |
148
+ ))
149
+ end
150
+
151
+ def test_linebreak_empty_lines
152
+ assert_context(
153
+ %Q(
154
+ A {
155
+ B name,a1: |
156
+ ),
157
+ %Q(
158
+ A {
159
+ B name,
160
+
161
+ a1: |
162
+ ))
163
+ end
164
+
165
+ def test_comment_annotation
166
+ assert_context(
167
+ %Q(
168
+ A {
169
+ B {
170
+ |F bla
171
+ ),
172
+ %Q(
173
+ A {
174
+ # bla
175
+ B {
176
+ C a1: v1, a2: "v2"
177
+ # bla
178
+ D {
179
+ E a1: 5
180
+ }
181
+ @ anno
182
+ |F bla
183
+ ))
184
+ end
185
+
186
+ def assert_context(expected, text)
187
+ # remove first and last lines
188
+ # these are empty because of the use of %Q
189
+ exp_lines = expected.split("\n")[1..-2]
190
+ exp_col = exp_lines.last.index("|")
191
+ exp_lines.last.sub!("|","")
192
+ in_lines = text.split("\n")[1..-2]
193
+ in_col = in_lines.last.index("|")
194
+ in_lines.last.sub!("|","")
195
+ ctx = RText::Frontend::Context.new
196
+ lines, out_col = ctx.extract(in_lines, in_col)
197
+ assert_equal exp_lines, lines
198
+ if exp_col && in_col
199
+ assert_equal exp_col, out_col
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+