rtext 0.4.0 → 0.5.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 +20 -0
- data/{README → README.rdoc} +5 -1
- data/RText_Protocol +444 -0
- data/Rakefile +10 -10
- data/lib/rtext/completer.rb +32 -26
- data/lib/rtext/context_builder.rb +113 -59
- data/lib/rtext/default_loader.rb +73 -8
- data/lib/rtext/default_service_provider.rb +30 -14
- data/lib/rtext/frontend/config.rb +58 -0
- data/lib/rtext/frontend/connector.rb +233 -0
- data/lib/rtext/frontend/connector_manager.rb +81 -0
- data/lib/rtext/frontend/context.rb +56 -0
- data/lib/rtext/generic.rb +13 -0
- data/lib/rtext/instantiator.rb +30 -7
- data/lib/rtext/language.rb +54 -27
- data/lib/rtext/link_detector.rb +57 -0
- data/lib/rtext/message_helper.rb +77 -0
- data/lib/rtext/parser.rb +19 -68
- data/lib/rtext/serializer.rb +18 -3
- data/lib/rtext/service.rb +182 -118
- data/lib/rtext/tokenizer.rb +102 -0
- data/test/completer_test.rb +327 -70
- data/test/context_builder_test.rb +671 -91
- data/test/instantiator_test.rb +153 -0
- data/test/integration/backend.out +10 -0
- data/test/integration/crash_on_request_editor.rb +12 -0
- data/test/integration/ecore_editor.rb +50 -0
- data/test/integration/frontend.log +25138 -0
- data/test/integration/model/invalid_encoding.invenc +2 -0
- data/test/integration/model/test.crash_on_request +18 -0
- data/test/integration/model/test.crashing_backend +18 -0
- data/test/integration/model/test.dont_open_socket +0 -0
- data/test/integration/model/test.invalid_cmd_line +0 -0
- data/test/integration/model/test.not_in_rtext +0 -0
- data/test/integration/model/test_large_with_errors.ect3 +43523 -0
- data/test/integration/model/test_metamodel.ect +18 -0
- data/test/integration/model/test_metamodel2.ect +5 -0
- data/test/integration/model/test_metamodel_error.ect2 +3 -0
- data/test/integration/model/test_metamodel_ok.ect2 +18 -0
- data/test/integration/test.rb +684 -0
- data/test/link_detector_test.rb +276 -0
- data/test/message_helper_test.rb +118 -0
- data/test/rtext_test.rb +4 -1
- data/test/serializer_test.rb +96 -1
- data/test/tokenizer_test.rb +125 -0
- metadata +36 -10
- data/RText_Plugin_Implementation_Guide +0 -268
- data/lib/rtext_plugin/connection_manager.rb +0 -59
data/lib/rtext/serializer.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rtext/language'
|
2
|
+
require 'rtext/generic'
|
2
3
|
|
3
4
|
module RText
|
4
5
|
|
@@ -57,6 +58,13 @@ class Serializer
|
|
57
58
|
write("##{l}")
|
58
59
|
end
|
59
60
|
end
|
61
|
+
# the annotation provider may modify the element
|
62
|
+
annotation = @lang.annotation_provider && @lang.annotation_provider.call(element)
|
63
|
+
if annotation
|
64
|
+
annotation.split(/\r?\n/).each do |l|
|
65
|
+
write("@#{l}")
|
66
|
+
end
|
67
|
+
end
|
60
68
|
headline = @lang.command_by_class(clazz.instanceClass)
|
61
69
|
raise "no command name for class #{clazz.instanceClass.to_s}" unless headline
|
62
70
|
args = []
|
@@ -110,8 +118,15 @@ class Serializer
|
|
110
118
|
values = element.getGenericAsArray(feature.name).compact
|
111
119
|
result = []
|
112
120
|
arg_format = @lang.argument_format(feature)
|
113
|
-
values.
|
114
|
-
if
|
121
|
+
values.each_with_index do |v, index|
|
122
|
+
if v.is_a?(RText::Generic)
|
123
|
+
str = v.string.split("%>").first
|
124
|
+
if str.index(">")
|
125
|
+
result << "<%#{str}%>"
|
126
|
+
else
|
127
|
+
result << "<#{str}>"
|
128
|
+
end
|
129
|
+
elsif feature.eType.instanceClass == Integer
|
115
130
|
if arg_format
|
116
131
|
result << sprintf(arg_format, v)
|
117
132
|
else
|
@@ -147,7 +162,7 @@ class Serializer
|
|
147
162
|
gsub("\r","\\r").gsub("\t","\\t").gsub("\f","\\f").gsub("\b","\\b")}\""
|
148
163
|
end
|
149
164
|
elsif feature.is_a?(RGen::ECore::EReference)
|
150
|
-
result << @lang.identifier_provider.call(v, element)
|
165
|
+
result << @lang.identifier_provider.call(v, element, feature, index)
|
151
166
|
end
|
152
167
|
end
|
153
168
|
if result.size > 1
|
data/lib/rtext/service.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'rtext/completer'
|
3
3
|
require 'rtext/context_builder'
|
4
|
+
require 'rtext/message_helper'
|
5
|
+
require 'rtext/link_detector'
|
4
6
|
|
5
7
|
module RText
|
6
8
|
|
7
9
|
class Service
|
10
|
+
include RText::MessageHelper
|
11
|
+
|
8
12
|
PortRangeStart = 9001
|
9
13
|
PortRangeEnd = 9100
|
10
14
|
|
@@ -18,170 +22,230 @@ class Service
|
|
18
22
|
# :logger
|
19
23
|
# a logger object on which the service will write its logging output
|
20
24
|
#
|
21
|
-
|
22
|
-
|
25
|
+
# :on_startup:
|
26
|
+
# a Proc which is called right after the service has started up
|
27
|
+
# can be used to output version information
|
28
|
+
#
|
29
|
+
def initialize(service_provider, options={})
|
23
30
|
@service_provider = service_provider
|
24
|
-
@completer = RText::Completer.new(lang)
|
25
31
|
@timeout = options[:timeout] || 60
|
26
32
|
@logger = options[:logger]
|
33
|
+
@on_startup = options[:on_startup]
|
27
34
|
end
|
28
35
|
|
29
36
|
def run
|
30
|
-
|
31
|
-
puts "RText service, listening on port #{
|
37
|
+
server = create_server
|
38
|
+
puts "RText service, listening on port #{server.addr[1]}"
|
39
|
+
@on_startup.call if @on_startup
|
32
40
|
$stdout.flush
|
33
41
|
|
34
42
|
last_access_time = Time.now
|
35
43
|
last_flush_time = Time.now
|
36
|
-
stop_requested = false
|
37
|
-
|
44
|
+
@stop_requested = false
|
45
|
+
sockets = []
|
46
|
+
request_data = {}
|
47
|
+
while !@stop_requested
|
38
48
|
begin
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
49
|
+
sock = server.accept_nonblock
|
50
|
+
sock.sync = true
|
51
|
+
sockets << sock
|
52
|
+
@logger.info "accepted connection" if @logger
|
53
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR, Errno::EWOULDBLOCK
|
54
|
+
rescue Exception => e
|
55
|
+
@logger.warn "unexpected exception during socket accept: #{e.class}"
|
56
|
+
end
|
57
|
+
sockets.dup.each do |sock|
|
58
|
+
data = nil
|
59
|
+
begin
|
60
|
+
data = sock.read_nonblock(100000)
|
61
|
+
rescue Errno::EWOULDBLOCK
|
62
|
+
rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
|
63
|
+
sock.close
|
64
|
+
request_data[sock] = nil
|
65
|
+
sockets.delete(sock)
|
66
|
+
rescue Exception => e
|
67
|
+
# catch Exception to make sure we don't crash due to unexpected exceptions
|
68
|
+
@logger.warn "unexpected exception during socket read: #{e.class}"
|
69
|
+
sock.close
|
70
|
+
request_data[sock] = nil
|
71
|
+
sockets.delete(sock)
|
45
72
|
end
|
46
|
-
|
73
|
+
if data
|
74
|
+
last_access_time = Time.now
|
75
|
+
request_data[sock] ||= ""
|
76
|
+
request_data[sock].concat(data)
|
77
|
+
while obj = extract_message(request_data[sock])
|
78
|
+
message_received(sock, obj)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
IO.select([server] + sockets, [], [], 1)
|
83
|
+
if Time.now > last_access_time + @timeout
|
84
|
+
@logger.info("RText service, stopping now (timeout)") if @logger
|
85
|
+
break
|
47
86
|
end
|
48
87
|
if Time.now > last_flush_time + FlushInterval
|
49
88
|
$stdout.flush
|
50
89
|
last_flush_time = Time.now
|
51
90
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
response = get_problems(lines, :with_severity => true, :on_progress => lambda do |frag, num_frags|
|
69
|
-
progress_index += 1
|
70
|
-
num_frags = 1 if num_frags < 1
|
71
|
-
progress = ["progress: #{progress_index*100/num_frags}"]
|
72
|
-
send_response(progress, invocation_id, socket, from, :incremental => true)
|
73
|
-
end)
|
74
|
-
when "get_reference_targets"
|
75
|
-
response = get_reference_targets(lines)
|
76
|
-
when "get_elements"
|
77
|
-
response = get_open_element_choices(lines)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def message_received(sock, obj)
|
95
|
+
if check_request(obj)
|
96
|
+
@logger.debug("request: "+obj.inspect) if @logger
|
97
|
+
response = { "type" => "response", "invocation_id" => obj["invocation_id"] }
|
98
|
+
case obj["command"]
|
99
|
+
when "load_model"
|
100
|
+
load_model(sock, obj, response)
|
101
|
+
when "content_complete"
|
102
|
+
content_complete(sock, obj, response)
|
103
|
+
when "link_targets"
|
104
|
+
link_targets(sock, obj, response)
|
105
|
+
when "find_elements"
|
106
|
+
find_elements(sock, obj, response)
|
78
107
|
when "stop"
|
79
|
-
response = []
|
80
108
|
@logger.info("RText service, stopping now (stop requested)") if @logger
|
81
|
-
stop_requested = true
|
109
|
+
@stop_requested = true
|
82
110
|
else
|
83
|
-
@logger.
|
84
|
-
response =
|
111
|
+
@logger.warn("unknown command #{obj["command"]}") if @logger
|
112
|
+
response["type"] = "unknown_command_error"
|
113
|
+
response["command"] = obj["command"]
|
85
114
|
end
|
86
|
-
|
115
|
+
@logger.debug("response: "+response.inspect) if response && @logger
|
116
|
+
send_response(sock, response)
|
87
117
|
end
|
88
118
|
end
|
89
119
|
|
90
120
|
private
|
91
121
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if options[:incremental] || response.size > 0
|
102
|
-
packet_lines.unshift("more\n")
|
103
|
-
else
|
104
|
-
packet_lines.unshift("last\n")
|
105
|
-
end
|
106
|
-
packet_lines.unshift("#{invocation_id}\n")
|
107
|
-
socket.send(packet_lines.join, 0, from[2], from[1])
|
108
|
-
break if response.size == 0
|
122
|
+
def check_request(obj)
|
123
|
+
if obj["type"] != "request"
|
124
|
+
@logger.warn("received message is not a request") if @logger
|
125
|
+
false
|
126
|
+
elsif !obj["invocation_id"].is_a?(Integer)
|
127
|
+
@logger.warn("invalid invocation id #{obj["invocation_id"]}") if @logger
|
128
|
+
false
|
129
|
+
else
|
130
|
+
true
|
109
131
|
end
|
110
132
|
end
|
111
133
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
134
|
+
def load_model(sock, request, response)
|
135
|
+
problems = @service_provider.get_problems(
|
136
|
+
:on_progress => lambda do |frag, work_done, work_overall|
|
137
|
+
work_overall = 1 if work_overall < 1
|
138
|
+
work_done = work_overall if work_done > work_overall
|
139
|
+
work_done = 0 if work_done < 0
|
140
|
+
send_response(sock, {
|
141
|
+
"type" => "progress",
|
142
|
+
"invocation_id" => request["invocation_id"],
|
143
|
+
"percentage" => work_done*100/work_overall
|
144
|
+
})
|
145
|
+
end)
|
146
|
+
total = 0
|
147
|
+
response["problems"] = problems.collect do |fp|
|
148
|
+
{ "file" => fp.file,
|
149
|
+
"problems" => fp.problems.collect do |p|
|
150
|
+
total += 1
|
151
|
+
{ "severity" => "error", "line" => p.line, "message" => p.message }
|
152
|
+
end }
|
121
153
|
end
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
|
-
def refresh(lines)
|
126
|
-
@service_provider.load_model
|
127
|
-
[]
|
154
|
+
response["total_problems"] = total
|
128
155
|
end
|
129
156
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
157
|
+
def content_complete(sock, request, response)
|
158
|
+
# column numbers start at 1
|
159
|
+
linepos = request["column"]-1
|
160
|
+
lines = request["context"]
|
161
|
+
lang = @service_provider.language
|
162
|
+
response["options"] = []
|
163
|
+
return unless lang
|
164
|
+
context = ContextBuilder.build_context(lang, lines, linepos)
|
165
|
+
@logger.debug("context element: #{lang.identifier_provider.call(context.element, nil, nil, nil)}") \
|
166
|
+
if context && context.element && @logger
|
167
|
+
completer = RText::Completer.new(lang)
|
168
|
+
options = completer.complete(context, lambda {|ref|
|
137
169
|
@service_provider.get_reference_completion_options(ref, context).collect {|o|
|
138
170
|
Completer::CompletionOption.new(o.identifier, "<#{o.type}>")}
|
139
171
|
})
|
140
|
-
options.collect
|
141
|
-
"#{o.text}
|
142
|
-
}
|
143
|
-
end
|
144
|
-
|
145
|
-
def get_problems(lines, options={})
|
146
|
-
result = []
|
147
|
-
severity = options[:with_severity] ? "e;" : ""
|
148
|
-
@service_provider.get_problems(:on_progress => options[:on_progress]).each do |fp|
|
149
|
-
result << fp.file+"\n"
|
150
|
-
fp.problems.each do |p|
|
151
|
-
result << "#{severity}#{p.line};#{p.message}\n"
|
152
|
-
end
|
172
|
+
response["options"] = options.collect do |o|
|
173
|
+
{ "insert" => o.text, "display" => "#{o.text} #{o.extra}" }
|
153
174
|
end
|
154
|
-
result
|
155
175
|
end
|
156
176
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
177
|
+
def link_targets(sock, request, response)
|
178
|
+
# column numbers start at 1
|
179
|
+
linepos = request["column"]
|
180
|
+
lines = request["context"]
|
181
|
+
lang = @service_provider.language
|
182
|
+
response["targets"] = []
|
183
|
+
return unless lang
|
184
|
+
link_descriptor = RText::LinkDetector.new(lang).detect(lines, linepos)
|
185
|
+
if link_descriptor
|
186
|
+
response["begin_column"] = link_descriptor.scol
|
187
|
+
response["end_column"] = link_descriptor.ecol
|
188
|
+
targets = []
|
189
|
+
if link_descriptor.backward
|
190
|
+
@service_provider.get_referencing_elements(
|
191
|
+
link_descriptor.value, link_descriptor.element, link_descriptor.feature, link_descriptor.index).each do |t|
|
192
|
+
targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
|
170
193
|
end
|
171
194
|
else
|
172
|
-
@service_provider.get_reference_targets(
|
173
|
-
|
195
|
+
@service_provider.get_reference_targets(
|
196
|
+
link_descriptor.value, link_descriptor.element, link_descriptor.feature, link_descriptor.index).each do |t|
|
197
|
+
targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
|
174
198
|
end
|
175
199
|
end
|
200
|
+
response["targets"] = targets
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def find_elements(sock, request, response)
|
205
|
+
pattern = request["search_pattern"]
|
206
|
+
total = 0
|
207
|
+
response["elements"] = @service_provider.get_open_element_choices(pattern).collect do |c|
|
208
|
+
total += 1
|
209
|
+
{ "display" => c.display_name, "file" => c.file, "line" => c.line }
|
210
|
+
end
|
211
|
+
response["total_elements"] = total
|
212
|
+
end
|
213
|
+
|
214
|
+
def send_response(sock, response)
|
215
|
+
if response
|
216
|
+
begin
|
217
|
+
sock.write(serialize_message(response))
|
218
|
+
sock.flush
|
219
|
+
# if there is an exception, the next read should shutdown the connection properly
|
220
|
+
rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
|
221
|
+
rescue Exception => e
|
222
|
+
# catch Exception to make sure we don't crash due to unexpected exceptions
|
223
|
+
@logger.warn "unexpected exception during socket write: #{e.class}"
|
224
|
+
end
|
176
225
|
end
|
177
|
-
result
|
178
226
|
end
|
179
227
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
228
|
+
def create_server
|
229
|
+
port = PortRangeStart
|
230
|
+
serv = nil
|
231
|
+
begin
|
232
|
+
serv = TCPServer.new("localhost", port)
|
233
|
+
if serv.addr[0] == "AF_INET6"
|
234
|
+
# we need to make sure that two RText server won't connect on the same port but with IPv4 and IPv6
|
235
|
+
# therefore we always use the IPv4 interface, try again to get the IPv4 interface
|
236
|
+
# note that using "127.0.0.1" instead of localhost would always get us the IPv4 interface on the
|
237
|
+
# first try, however, on some machines using the IP address instead of localhost doesn't work
|
238
|
+
# (EACCES error was seen, possibly due to some specific firewall setup)
|
239
|
+
serv2 = TCPServer.new("localhost", port)
|
240
|
+
serv.close
|
241
|
+
serv = serv2
|
242
|
+
end
|
243
|
+
rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT
|
244
|
+
port += 1
|
245
|
+
retry if port <= PortRangeEnd
|
246
|
+
raise
|
184
247
|
end
|
248
|
+
serv
|
185
249
|
end
|
186
250
|
|
187
251
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rtext/generic'
|
2
|
+
|
3
|
+
module RText
|
4
|
+
|
5
|
+
module Tokenizer
|
6
|
+
|
7
|
+
Token = Struct.new(:kind, :value, :line, :scol, :ecol)
|
8
|
+
|
9
|
+
def tokenize(str, reference_regexp, options={})
|
10
|
+
result = []
|
11
|
+
on_command_token_proc = options[:on_command_token]
|
12
|
+
str.split(/\r?\n/).each_with_index do |str, idx|
|
13
|
+
idx += 1
|
14
|
+
if str =~ /^\s*([\#@])(.*)/
|
15
|
+
if $1 == "#"
|
16
|
+
result << Token.new(:comment, $2, idx, str.size-$2.size, str.size)
|
17
|
+
else
|
18
|
+
result << Token.new(:annotation, $2, idx, str.size-$2.size, str.size)
|
19
|
+
end
|
20
|
+
else
|
21
|
+
col = 1
|
22
|
+
first_token_in_line = true
|
23
|
+
until str.empty?
|
24
|
+
whitespace = false
|
25
|
+
case str
|
26
|
+
when reference_regexp
|
27
|
+
str = $'
|
28
|
+
result << Token.new(:reference, $&, idx, col, col+$&.size-1)
|
29
|
+
col += $&.size
|
30
|
+
when /\A[-+]?\d+\.\d+(?:e[+-]\d+)?\b/
|
31
|
+
str = $'
|
32
|
+
result << Token.new(:float, $&.to_f, idx, col, col+$&.size-1)
|
33
|
+
col += $&.size
|
34
|
+
when /\A0[xX][0-9a-fA-F]+\b/
|
35
|
+
str = $'
|
36
|
+
result << Token.new(:integer, $&.to_i(16), idx, col, col+$&.size-1)
|
37
|
+
col += $&.size
|
38
|
+
when /\A[-+]?\d+\b/
|
39
|
+
str = $'
|
40
|
+
result << Token.new(:integer, $&.to_i, idx, col, col+$&.size-1)
|
41
|
+
col += $&.size
|
42
|
+
when /\A"((?:[^"\\]|\\.)*)"/
|
43
|
+
str = $'
|
44
|
+
match = $&
|
45
|
+
result << Token.new(:string, $1.
|
46
|
+
gsub('\\\\','\\').
|
47
|
+
gsub('\\"','"').
|
48
|
+
gsub('\\n',"\n").
|
49
|
+
gsub('\\r',"\r").
|
50
|
+
gsub('\\t',"\t").
|
51
|
+
gsub('\\f',"\f").
|
52
|
+
gsub('\\b',"\b"), idx, col, col+match.size-1)
|
53
|
+
col += match.size
|
54
|
+
when /\A(?:true|false)\b/
|
55
|
+
str = $'
|
56
|
+
result << Token.new(:boolean, $& == "true", idx, col, col+$&.size-1)
|
57
|
+
col += $&.size
|
58
|
+
when /\A([a-zA-Z_]\w*)\b(?:\s*:)?/
|
59
|
+
str = $'
|
60
|
+
if $&[-1] == ?:
|
61
|
+
result << Token.new(:label, $1, idx, col, col+$&.size-1)
|
62
|
+
else
|
63
|
+
result << Token.new(:identifier, $&, idx, col, col+$&.size-1)
|
64
|
+
if first_token_in_line && on_command_token_proc
|
65
|
+
on_command_token_proc.call
|
66
|
+
end
|
67
|
+
end
|
68
|
+
col += $&.size
|
69
|
+
when /\A[\{\}\[\]:,]/
|
70
|
+
str = $'
|
71
|
+
result << Token.new($&, nil, idx, col, col+$&.size-1)
|
72
|
+
col += $&.size
|
73
|
+
when /\A#(.*)/
|
74
|
+
str = ""
|
75
|
+
result << Token.new(:comment, $1, idx, col, col+$&.size-1)
|
76
|
+
when /\A\s+/
|
77
|
+
str = $'
|
78
|
+
col += $&.size
|
79
|
+
whitespace = true
|
80
|
+
# ignore
|
81
|
+
when /\A<%((?:(?!%>).)*)%>/, /\A<([^>]*)>/
|
82
|
+
str = $'
|
83
|
+
result << Token.new(:generic, RText::Generic.new($1), idx, col, col+$&.size-1)
|
84
|
+
col += $&.size
|
85
|
+
when /\A\S+/
|
86
|
+
str = $'
|
87
|
+
result << Token.new(:error, $&, idx, col, col+$&.size-1)
|
88
|
+
col += $&.size
|
89
|
+
end
|
90
|
+
first_token_in_line = false unless whitespace
|
91
|
+
end
|
92
|
+
end
|
93
|
+
result << Token.new(:newline, nil, idx) \
|
94
|
+
unless result.empty? || result.last.kind == :newline
|
95
|
+
end
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|