rdoc 7.2.0 → 8.0.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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +3 -4
- data/LICENSE.rdoc +4 -0
- data/README.md +43 -2
- data/doc/markup_reference/markdown.md +104 -3
- data/lib/rdoc/code_object/alias.rb +2 -8
- data/lib/rdoc/code_object/any_method.rb +11 -6
- data/lib/rdoc/code_object/attr.rb +11 -6
- data/lib/rdoc/code_object/class_module.rb +62 -32
- data/lib/rdoc/code_object/constant.rb +29 -3
- data/lib/rdoc/code_object/context/section.rb +4 -35
- data/lib/rdoc/code_object/context.rb +39 -34
- data/lib/rdoc/code_object/method_attr.rb +9 -15
- data/lib/rdoc/code_object/mixin.rb +2 -2
- data/lib/rdoc/code_object/top_level.rb +9 -3
- data/lib/rdoc/code_object.rb +2 -4
- data/lib/rdoc/comment.rb +0 -65
- data/lib/rdoc/cross_reference.rb +7 -27
- data/lib/rdoc/encoding.rb +3 -3
- data/lib/rdoc/generator/aliki.rb +17 -0
- data/lib/rdoc/generator/darkfish.rb +12 -6
- data/lib/rdoc/generator/json_index.rb +2 -2
- data/lib/rdoc/generator/markup.rb +56 -31
- data/lib/rdoc/generator/template/aliki/DESIGN.md +536 -0
- data/lib/rdoc/generator/template/aliki/_aside_toc.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/_head.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/_sidebar_extends.rhtml +8 -6
- data/lib/rdoc/generator/template/aliki/_sidebar_includes.rhtml +8 -6
- data/lib/rdoc/generator/template/aliki/_sidebar_installed.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +2 -2
- data/lib/rdoc/generator/template/aliki/_sidebar_sections.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/_sidebar_toggle.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/class.rhtml +56 -46
- data/lib/rdoc/generator/template/aliki/css/rdoc.css +337 -111
- data/lib/rdoc/generator/template/aliki/index.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/js/aliki.js +20 -18
- data/lib/rdoc/generator/template/aliki/page.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/servlet_not_found.rhtml +1 -1
- data/lib/rdoc/generator/template/aliki/servlet_root.rhtml +2 -2
- data/lib/rdoc/generator/template/darkfish/_sidebar_extends.rhtml +8 -6
- data/lib/rdoc/generator/template/darkfish/_sidebar_includes.rhtml +8 -6
- data/lib/rdoc/generator/template/darkfish/_sidebar_installed.rhtml +1 -1
- data/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml +1 -1
- data/lib/rdoc/generator/template/darkfish/_sidebar_sections.rhtml +1 -1
- data/lib/rdoc/generator/template/darkfish/_sidebar_table_of_contents.rhtml +5 -5
- data/lib/rdoc/generator/template/darkfish/class.rhtml +18 -21
- data/lib/rdoc/generator/template/darkfish/css/rdoc.css +0 -1
- data/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +3 -3
- data/lib/rdoc/i18n/text.rb +3 -3
- data/lib/rdoc/markdown.kpeg +15 -10
- data/lib/rdoc/markdown.rb +289 -104
- data/lib/rdoc/markup/document.rb +2 -2
- data/lib/rdoc/markup/formatter.rb +24 -34
- data/lib/rdoc/markup/heading.rb +1 -4
- data/lib/rdoc/markup/indented_paragraph.rb +1 -1
- data/lib/rdoc/markup/list.rb +2 -2
- data/lib/rdoc/markup/list_item.rb +2 -2
- data/lib/rdoc/markup/pre_process.rb +0 -25
- data/lib/rdoc/markup/to_ansi.rb +1 -1
- data/lib/rdoc/markup/to_bs.rb +1 -1
- data/lib/rdoc/markup/to_html.rb +131 -53
- data/lib/rdoc/markup/to_html_crossref.rb +97 -71
- data/lib/rdoc/markup/to_html_snippet.rb +5 -5
- data/lib/rdoc/markup/to_joined_paragraph.rb +0 -5
- data/lib/rdoc/markup/to_label.rb +2 -2
- data/lib/rdoc/markup/to_markdown.rb +1 -1
- data/lib/rdoc/markup/to_rdoc.rb +2 -2
- data/lib/rdoc/markup/to_table_of_contents.rb +1 -1
- data/lib/rdoc/markup/to_tt_only.rb +0 -7
- data/lib/rdoc/markup/verbatim.rb +1 -1
- data/lib/rdoc/options.rb +36 -51
- data/lib/rdoc/parser/c.rb +7 -6
- data/lib/rdoc/parser/rbs.rb +275 -0
- data/lib/rdoc/parser/ruby.rb +954 -2066
- data/lib/rdoc/parser/ruby_colorizer.rb +253 -0
- data/lib/rdoc/parser.rb +3 -2
- data/lib/rdoc/rbs_helper.rb +186 -0
- data/lib/rdoc/rdoc.rb +196 -24
- data/lib/rdoc/ri/driver.rb +8 -2
- data/lib/rdoc/ri/paths.rb +1 -1
- data/lib/rdoc/{servlet.rb → ri/servlet.rb} +5 -5
- data/lib/rdoc/ri.rb +4 -3
- data/lib/rdoc/rubygems_hook.rb +11 -11
- data/lib/rdoc/server.rb +460 -0
- data/lib/rdoc/stats.rb +147 -124
- data/lib/rdoc/store.rb +212 -4
- data/lib/rdoc/task.rb +16 -15
- data/lib/rdoc/text.rb +1 -118
- data/lib/rdoc/token_stream.rb +11 -33
- data/lib/rdoc/version.rb +1 -1
- data/lib/rdoc.rb +35 -7
- data/lib/rubygems_plugin.rb +2 -11
- data/rdoc-logo.svg +43 -0
- data/rdoc.gemspec +6 -4
- metadata +35 -18
- data/lib/rdoc/code_object/anon_class.rb +0 -10
- data/lib/rdoc/code_object/ghost_method.rb +0 -6
- data/lib/rdoc/code_object/meta_method.rb +0 -6
- data/lib/rdoc/parser/prism_ruby.rb +0 -1112
- data/lib/rdoc/parser/ripper_state_lex.rb +0 -302
- data/lib/rdoc/parser/ruby_tools.rb +0 -163
data/lib/rdoc/server.rb
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'erb'
|
|
6
|
+
require 'set'
|
|
7
|
+
require 'uri'
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# A minimal HTTP server for live-reloading RDoc documentation.
|
|
11
|
+
#
|
|
12
|
+
# Uses Ruby's built-in +TCPServer+ (no external dependencies).
|
|
13
|
+
#
|
|
14
|
+
# Used by <tt>rdoc --server</tt> to let developers preview documentation
|
|
15
|
+
# while editing source files. Parses sources once on startup, watches for
|
|
16
|
+
# file changes, re-parses only the changed files, and auto-refreshes the
|
|
17
|
+
# browser via a simple polling script.
|
|
18
|
+
|
|
19
|
+
class RDoc::Server
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Returns a live-reload polling script with the given +last_change_time+
|
|
23
|
+
# embedded so the browser knows the exact timestamp of the content it
|
|
24
|
+
# received. This avoids a race where a change that occurs between page
|
|
25
|
+
# generation and the first poll would be silently skipped.
|
|
26
|
+
|
|
27
|
+
def self.live_reload_script(last_change_time)
|
|
28
|
+
<<~JS
|
|
29
|
+
<script>
|
|
30
|
+
(function() {
|
|
31
|
+
var lastChange = #{last_change_time.to_json};
|
|
32
|
+
setInterval(function() {
|
|
33
|
+
fetch('/__status').then(function(r) { return r.json(); }).then(function(data) {
|
|
34
|
+
if (data.last_change > lastChange) location.reload();
|
|
35
|
+
lastChange = data.last_change;
|
|
36
|
+
}).catch(function() {});
|
|
37
|
+
}, 1000);
|
|
38
|
+
})();
|
|
39
|
+
</script>
|
|
40
|
+
JS
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
CONTENT_TYPES = {
|
|
44
|
+
'.html' => 'text/html',
|
|
45
|
+
'.css' => 'text/css',
|
|
46
|
+
'.js' => 'application/javascript',
|
|
47
|
+
'.json' => 'application/json',
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
STATUS_TEXTS = {
|
|
51
|
+
200 => 'OK',
|
|
52
|
+
400 => 'Bad Request',
|
|
53
|
+
404 => 'Not Found',
|
|
54
|
+
405 => 'Method Not Allowed',
|
|
55
|
+
500 => 'Internal Server Error',
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
class FileChanges # :nodoc:
|
|
59
|
+
attr_reader :changed_files, :removed_files
|
|
60
|
+
|
|
61
|
+
def initialize(rdoc)
|
|
62
|
+
@rdoc = rdoc
|
|
63
|
+
@changed_files = []
|
|
64
|
+
@removed_files = []
|
|
65
|
+
@reload_rbs_signatures = false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def record_changed(file)
|
|
69
|
+
reload_rbs_signatures_if_needed file
|
|
70
|
+
changed_files << file
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def record_removed(file)
|
|
74
|
+
reload_rbs_signatures_if_needed file
|
|
75
|
+
removed_files << file
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reload_rbs_signatures?
|
|
79
|
+
@reload_rbs_signatures
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def source_files_changed?
|
|
83
|
+
!changed_files.empty? || !removed_files.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def reload_rbs_signatures_if_needed(file)
|
|
89
|
+
@reload_rbs_signatures = true if @rdoc.auto_discovered_rbs_signature_file?(file)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Creates a new server.
|
|
95
|
+
#
|
|
96
|
+
# +rdoc+ is the RDoc::RDoc instance that has already parsed the source
|
|
97
|
+
# files.
|
|
98
|
+
# +port+ is the TCP port to listen on.
|
|
99
|
+
|
|
100
|
+
def initialize(rdoc, port)
|
|
101
|
+
@rdoc = rdoc
|
|
102
|
+
@options = rdoc.options
|
|
103
|
+
@store = rdoc.store
|
|
104
|
+
@port = port
|
|
105
|
+
|
|
106
|
+
# Silence stats output — the server prints its own timing.
|
|
107
|
+
@rdoc.stats.verbosity = 0
|
|
108
|
+
@generator = create_generator
|
|
109
|
+
@template_dir = File.expand_path(@generator.template_dir)
|
|
110
|
+
@page_cache = {}
|
|
111
|
+
@last_change_time = Time.now.to_f
|
|
112
|
+
@mutex = Mutex.new
|
|
113
|
+
@running = false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Starts the server. Blocks until interrupted.
|
|
118
|
+
|
|
119
|
+
def start
|
|
120
|
+
@tcp_server = TCPServer.new('127.0.0.1', @port)
|
|
121
|
+
@running = true
|
|
122
|
+
|
|
123
|
+
@watcher_thread = start_watcher(@rdoc.watch_files)
|
|
124
|
+
|
|
125
|
+
url = "http://localhost:#{@port}"
|
|
126
|
+
$stderr.puts "\nServing documentation at: \e]8;;#{url}\e\\#{url}\e]8;;\e\\"
|
|
127
|
+
$stderr.puts "Press Ctrl+C to stop.\n\n"
|
|
128
|
+
|
|
129
|
+
loop do
|
|
130
|
+
client = @tcp_server.accept
|
|
131
|
+
Thread.new(client) { |c| handle_client(c) }
|
|
132
|
+
end
|
|
133
|
+
rescue Interrupt
|
|
134
|
+
# Ctrl+C
|
|
135
|
+
ensure
|
|
136
|
+
@running = false
|
|
137
|
+
@tcp_server&.close
|
|
138
|
+
@watcher_thread&.join(2)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def measure
|
|
144
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
145
|
+
yield
|
|
146
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(1)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def create_generator
|
|
150
|
+
gen = RDoc::Generator::Aliki.new(@store, @options)
|
|
151
|
+
gen.file_output = false
|
|
152
|
+
gen.asset_rel_path = ''
|
|
153
|
+
gen.setup
|
|
154
|
+
gen
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
##
|
|
158
|
+
# Reads an HTTP request from +client+ and dispatches to the router.
|
|
159
|
+
|
|
160
|
+
def handle_client(client)
|
|
161
|
+
client.binmode
|
|
162
|
+
|
|
163
|
+
return unless IO.select([client], nil, nil, 5)
|
|
164
|
+
|
|
165
|
+
request_line = client.gets("\n")
|
|
166
|
+
return unless request_line
|
|
167
|
+
|
|
168
|
+
method, request_uri, = request_line.split(' ', 3)
|
|
169
|
+
return write_response(client, 400, 'text/plain', 'Bad Request') unless request_uri
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
path = URI.parse(request_uri).path
|
|
173
|
+
rescue URI::InvalidURIError
|
|
174
|
+
return write_response(client, 400, 'text/plain', 'Bad Request')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
while (line = client.gets("\n"))
|
|
178
|
+
break if line.strip.empty?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
unless method == 'GET'
|
|
182
|
+
return write_response(client, 405, 'text/plain', 'Method Not Allowed')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if path.start_with?('/__') || %r{\A/(?:css|js)/}.match?(path)
|
|
186
|
+
status, content_type, body = route(path)
|
|
187
|
+
else
|
|
188
|
+
duration_ms = measure do
|
|
189
|
+
status, content_type, body = route(path)
|
|
190
|
+
end
|
|
191
|
+
$stderr.puts "#{status} #{path} (#{duration_ms}ms)"
|
|
192
|
+
end
|
|
193
|
+
write_response(client, status, content_type, body)
|
|
194
|
+
rescue => e
|
|
195
|
+
write_response(client, 500, 'text/html', <<~HTML)
|
|
196
|
+
<!DOCTYPE html>
|
|
197
|
+
<html><body>
|
|
198
|
+
<h1>Internal Server Error</h1>
|
|
199
|
+
<pre>#{ERB::Util.html_escape e.message}\n#{ERB::Util.html_escape e.backtrace.join("\n")}</pre>
|
|
200
|
+
</body></html>
|
|
201
|
+
HTML
|
|
202
|
+
ensure
|
|
203
|
+
client.close rescue nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
##
|
|
207
|
+
# Routes a request path and returns [status, content_type, body].
|
|
208
|
+
|
|
209
|
+
def route(path)
|
|
210
|
+
case path
|
|
211
|
+
when '/__status'
|
|
212
|
+
t = @mutex.synchronize { @last_change_time }
|
|
213
|
+
[200, 'application/json', JSON.generate(last_change: t)]
|
|
214
|
+
when '/js/search_data.js'
|
|
215
|
+
# Search data is dynamically generated, not a static asset
|
|
216
|
+
serve_page(path)
|
|
217
|
+
when %r{\A/(?:css|js)/}
|
|
218
|
+
serve_asset(path)
|
|
219
|
+
else
|
|
220
|
+
serve_page(path)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
##
|
|
225
|
+
# Writes an HTTP/1.1 response to +client+.
|
|
226
|
+
|
|
227
|
+
def write_response(client, status, content_type, body)
|
|
228
|
+
body_bytes = body.b
|
|
229
|
+
|
|
230
|
+
header = +"HTTP/1.1 #{status} #{STATUS_TEXTS[status] || 'Unknown'}\r\n"
|
|
231
|
+
header << "Content-Type: #{content_type}\r\n"
|
|
232
|
+
header << "Content-Length: #{body_bytes.bytesize}\r\n"
|
|
233
|
+
header << "Connection: close\r\n"
|
|
234
|
+
header << "\r\n"
|
|
235
|
+
|
|
236
|
+
client.write(header)
|
|
237
|
+
client.write(body_bytes)
|
|
238
|
+
client.flush
|
|
239
|
+
rescue Errno::EPIPE
|
|
240
|
+
# Client disconnected before we finished writing — harmless.
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
##
|
|
244
|
+
# Serves a static asset (CSS, JS) from the Aliki template directory.
|
|
245
|
+
|
|
246
|
+
def serve_asset(path)
|
|
247
|
+
rel_path = path.delete_prefix("/")
|
|
248
|
+
asset_path = File.join(@generator.template_dir, rel_path)
|
|
249
|
+
real_asset = File.expand_path(asset_path)
|
|
250
|
+
|
|
251
|
+
unless real_asset.start_with?("#{@template_dir}/") && File.file?(real_asset)
|
|
252
|
+
return [404, 'text/plain', "Asset not found: #{rel_path}"]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
ext = File.extname(rel_path)
|
|
256
|
+
content_type = CONTENT_TYPES[ext] || 'application/octet-stream'
|
|
257
|
+
[200, content_type, File.read(real_asset)]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# Serves an HTML page, rendering from the generator or returning a cached
|
|
262
|
+
# version.
|
|
263
|
+
|
|
264
|
+
def serve_page(path)
|
|
265
|
+
name = path.delete_prefix("/")
|
|
266
|
+
name = 'index.html' if name.empty?
|
|
267
|
+
|
|
268
|
+
html = render_page(name)
|
|
269
|
+
|
|
270
|
+
unless html
|
|
271
|
+
not_found = @generator.generate_servlet_not_found(
|
|
272
|
+
"The page <kbd>#{ERB::Util.html_escape path}</kbd> was not found"
|
|
273
|
+
)
|
|
274
|
+
t = @mutex.synchronize { @last_change_time }
|
|
275
|
+
return [404, 'text/html', inject_live_reload(not_found || '', t)]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
ext = File.extname(name)
|
|
279
|
+
content_type = CONTENT_TYPES[ext] || 'text/html'
|
|
280
|
+
[200, content_type, html]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
##
|
|
284
|
+
# Renders a page through the Aliki generator and caches the result.
|
|
285
|
+
|
|
286
|
+
def render_page(name)
|
|
287
|
+
@mutex.synchronize do
|
|
288
|
+
return @page_cache[name] if @page_cache[name]
|
|
289
|
+
|
|
290
|
+
result = generate_page(name)
|
|
291
|
+
return nil unless result
|
|
292
|
+
|
|
293
|
+
result = inject_live_reload(result, @last_change_time) if name.end_with?('.html')
|
|
294
|
+
@page_cache[name] = result
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
##
|
|
299
|
+
# Dispatches to the appropriate generator method based on the page name.
|
|
300
|
+
|
|
301
|
+
def generate_page(name)
|
|
302
|
+
case name
|
|
303
|
+
when 'index.html'
|
|
304
|
+
@generator.generate_index
|
|
305
|
+
when 'table_of_contents.html'
|
|
306
|
+
@generator.generate_table_of_contents
|
|
307
|
+
when 'js/search_data.js'
|
|
308
|
+
"var search_data = #{JSON.generate(index: @generator.build_search_index)};"
|
|
309
|
+
else
|
|
310
|
+
text_name = name.chomp('.html')
|
|
311
|
+
class_name = text_name.gsub('/', '::')
|
|
312
|
+
|
|
313
|
+
if klass = @store.find_class_or_module(class_name)
|
|
314
|
+
@generator.generate_class(klass)
|
|
315
|
+
elsif page = @store.find_text_page(text_name.sub(/_([^_]*)\z/, '.\1'))
|
|
316
|
+
@generator.generate_page(page)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
##
|
|
322
|
+
# Injects the live-reload polling script before +</body>+.
|
|
323
|
+
|
|
324
|
+
def inject_live_reload(html, last_change_time)
|
|
325
|
+
html.sub('</body>', "#{self.class.live_reload_script(last_change_time)}</body>")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
##
|
|
329
|
+
# Starts a background thread that polls source file mtimes and triggers
|
|
330
|
+
# re-parsing when changes are detected.
|
|
331
|
+
|
|
332
|
+
def start_watcher(source_files)
|
|
333
|
+
@file_mtimes = file_mtimes_for(source_files)
|
|
334
|
+
|
|
335
|
+
Thread.new do
|
|
336
|
+
while @running
|
|
337
|
+
begin
|
|
338
|
+
sleep 1
|
|
339
|
+
check_for_changes
|
|
340
|
+
rescue => e
|
|
341
|
+
$stderr.puts "RDoc server watcher error: #{e.message}"
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def file_mtimes_for(files)
|
|
348
|
+
files.each_with_object({}) do |f, h|
|
|
349
|
+
h[f] = RDoc.safe_mtime(f)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
##
|
|
354
|
+
# Checks for modified, new, and deleted files. Returns true if any
|
|
355
|
+
# changes were found and processed.
|
|
356
|
+
|
|
357
|
+
def check_for_changes
|
|
358
|
+
changes = FileChanges.new @rdoc
|
|
359
|
+
current_files = current_watch_files
|
|
360
|
+
current_file_set = current_files.to_set
|
|
361
|
+
|
|
362
|
+
@file_mtimes.each_key do |file|
|
|
363
|
+
changes.record_removed file unless current_file_set.include? file
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
current_files.each do |file|
|
|
367
|
+
next unless file_changed? file
|
|
368
|
+
|
|
369
|
+
@file_mtimes[file] = nil unless @file_mtimes.key? file
|
|
370
|
+
changes.record_changed file
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
return false unless changes.source_files_changed?
|
|
374
|
+
|
|
375
|
+
reparse_and_refresh changes
|
|
376
|
+
true
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
##
|
|
380
|
+
# Re-parses changed files, removes deleted files from the store,
|
|
381
|
+
# refreshes the generator, and invalidates caches.
|
|
382
|
+
|
|
383
|
+
def reparse_and_refresh(changes)
|
|
384
|
+
@mutex.synchronize do
|
|
385
|
+
remove_files changes.removed_files
|
|
386
|
+
reparse_files changes.changed_files
|
|
387
|
+
reload_rbs_signatures if changes.reload_rbs_signatures?
|
|
388
|
+
@store.complete(@options.visibility)
|
|
389
|
+
@store.invalidate_type_name_lookup if changes.source_files_changed?
|
|
390
|
+
|
|
391
|
+
@generator.refresh_store_data
|
|
392
|
+
@page_cache.clear
|
|
393
|
+
@last_change_time = Time.now.to_f
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def current_watch_files
|
|
398
|
+
file_list = @rdoc.normalized_file_list(
|
|
399
|
+
@options.files.empty? ? [@options.root.to_s] : @options.files,
|
|
400
|
+
true, @options.exclude
|
|
401
|
+
)
|
|
402
|
+
@rdoc.remove_unparseable(file_list).keys | @rdoc.auto_discovered_rbs_signature_files
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def file_changed?(file)
|
|
406
|
+
return true unless @file_mtimes.key? file
|
|
407
|
+
|
|
408
|
+
old_mtime = @file_mtimes[file]
|
|
409
|
+
return true unless old_mtime
|
|
410
|
+
|
|
411
|
+
current_mtime = RDoc.safe_mtime(file)
|
|
412
|
+
current_mtime && current_mtime > old_mtime
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def remove_files(files)
|
|
416
|
+
return if files.empty?
|
|
417
|
+
|
|
418
|
+
$stderr.puts "Removed: #{files.join(', ')}"
|
|
419
|
+
files.each do |f|
|
|
420
|
+
@file_mtimes.delete(f)
|
|
421
|
+
relative = @rdoc.relative_path_for(f)
|
|
422
|
+
@store.clear_file_contributions(relative)
|
|
423
|
+
@store.remove_file(relative)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def reload_rbs_signatures
|
|
428
|
+
duration_ms = measure do
|
|
429
|
+
@rdoc.load_auto_discovered_rbs_signatures
|
|
430
|
+
@rdoc.record_auto_discovered_rbs_signature_mtimes
|
|
431
|
+
@rdoc.auto_discovered_rbs_signature_files.each do |file|
|
|
432
|
+
@file_mtimes[file] = RDoc.safe_mtime(file)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
$stderr.puts "Reloaded RBS signatures (#{duration_ms}ms)"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def reparse_files(files)
|
|
439
|
+
return if files.empty?
|
|
440
|
+
|
|
441
|
+
changed_file_names = []
|
|
442
|
+
duration_ms = measure do
|
|
443
|
+
files.each do |f|
|
|
444
|
+
relative = @rdoc.relative_path_for(f)
|
|
445
|
+
changed_file_names << relative
|
|
446
|
+
begin
|
|
447
|
+
@store.clear_file_contributions(relative, keep_position: true)
|
|
448
|
+
@rdoc.parse_file(f)
|
|
449
|
+
@file_mtimes[f] = RDoc.safe_mtime(f)
|
|
450
|
+
rescue => e
|
|
451
|
+
$stderr.puts "Error parsing #{f}: #{e.message}"
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
@store.cleanup_stale_contributions
|
|
456
|
+
end
|
|
457
|
+
$stderr.puts "Re-parsed #{changed_file_names.join(', ')} (#{duration_ms}ms)"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
end
|