st0012 · GitHub

@st0012 mentioned this pull request

Feb 21, 2026

Closed

@st0012

@st0012

st0012 marked this pull request as ready for review

February 21, 2026 17:58

kou

tompng

tompng

@st0012

kou

kou approved these changes Mar 1, 2026

tompng

@st0012

Merged

tompng

st0012 added a commit that referenced this pull request

Mar 5, 2026
Convert `ClassModule#comment_location` from an Array of `[comment,
location]` pairs to a Hash of `{ location => [comments] }`.
## Motivation
When a class like `RDoc` is documented across multiple files
(`lib/rdoc.rb`, `lib/rdoc/rubygems_hook.rb`, etc.), each file
contributes a comment. With the old Array of `[comment, location]`
pairs, there was no efficient way to look up or replace comments by
file. This matters for server live-reload (#1620): when a user edits
`lib/rdoc.rb`, we need to clear that file's comment and re-parse — but
preserve comments from other files **in their original order**. The
Array structure made this fragile because removing and re-appending an
entry moved it to the end, changing the order comments appeared on the
rendered page.
The Hash structure solves this: `{ location => [comments] }` allows O(1)
lookup by file, and Ruby hashes preserve insertion order — replacing a
key's value keeps it in position.
A secondary benefit: a class reopened in the same file now preserves all
its comments:
```ruby
# comment1
class A; end
# comment2
class A; end
```
With the old Array, the C parser had a special-case `delete_if` to
deduplicate same-location entries, while Ruby parsers accumulated
duplicates inconsistently. With the new Hash, each location maps to an
array of comments — both comments are preserved and rendered, matching
the cross-file behavior.
## Changes
- `add_comment`: appends to array per location —
`(@comment_location[location] ||= []) << comment`
- `parse`: uses `flat_map` to flatten per-location comment arrays into
Document parts
- `marshal_load` / `merge`: use `group_by(&:file)` to reconstruct arrays
from Documents
- `documented?`, `search_snippet`, `from_module`: updated for new value
shape
- `i18n/text.rb`: handles Hash with array values
- C parser `delete_if` special case removed (hash key naturally
deduplicates by location)

@st0012

tompng

Merged

Adds a built-in HTTP server for previewing documentation while editing
source files. Parses all sources on startup, watches for file changes,
re-parses only changed files, and auto-refreshes the browser.
Server implementation (lib/rdoc/server.rb):
- Uses Ruby's built-in TCPServer (no WEBrick or external dependencies)
- Persistent Aliki generator instance rendering to strings
- Thread-per-connection with Connection: close (no keep-alive)
- Background watcher thread polls file mtimes every 1 second
- Live reload via inline JS polling /__status endpoint
- New --server[=PORT] option (default 4000) and rdoc:server Rake task
- Moved RDoc::Servlet to RDoc::RI::Servlet (server mode uses new class)
Security:
- Binds to 127.0.0.1 only (localhost)
- Path traversal protection in asset serving via expand_path containment
- Proper HTTP error responses (400, 404, 405, 500)
- 5-second read timeout on client sockets
Concurrency:
- Mutex protects all store mutations, generator refresh, and cache
  invalidation as a single atomic operation
- Thread-safe last_change_time reads for the status endpoint
Correctness:
- Clears file contributions (methods, constants, comments, etc.) before
  re-parsing to prevent duplication, without removing shared namespaces
- Individual parse_file errors caught so one failure doesn't block others
- Store#remove_file recursively cleans nested classes/modules and C vars
- Watcher thread uses @running flag with clean shutdown via join
Co-authored-by: Sutou Kouhei <kou@clear-code.com>
- Embed last_change_time into the live-reload script at render time
  so the browser's initial timestamp matches the page content. This
  fixes a race where a change between page generation and the first
  poll would be silently skipped.
- Call clear_file_contributions for removed files (not just changed
  files) and remove classes/modules from the store when no files
  contribute to them anymore. This correctly handles reopened classes
  across multiple files and improves file deletion behavior.
Move `relative_path_for` from a private method on RDoc::Server to a
public method on RDoc::RDoc, eliminating the duplication with the
inline logic in `parse_file`.
Move `clear_file_contributions` from RDoc::Server to RDoc::Store
where it naturally belongs — it operates entirely on store internals
(files_hash, classes_hash, modules_hash).
Add tests for Store#clear_file_contributions covering single-file
removal, multi-file preservation, per-file cleanup of methods/
constants/includes, and no-op for nonexistent files.
- Use exit instead of return in document method for consistency
- Replace path.sub regex with delete_prefix in server.rb
- Use to_json for safe JavaScript value embedding
Also update clear_file_contributions for the Hash-based comment_location:
- Add keep_position: keyword for server re-parse (preserves key position)
- Add rebuild_comment_from_location method
- Server passes keep_position: true when re-parsing changed files
When keep_position: true is used during server re-parse, empty placeholder
entries persist in comment_location if the re-parsed file no longer defines
that class. Add Store#cleanup_stale_contributions to remove empty entries
and stale in_files references after all re-parsing is complete. If a class
has no remaining contributions, it is removed from the store.

@st0012

@st0012

@st0012

st0012 deleted the server-mode-retry branch

March 14, 2026 22:46

Read the original on github.com ↗