rtext 0.7.0 → 0.8.0.pre1

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