Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture overview

Ripscope is a single Rust executable. It reads C source files with its own scanner, stores each file’s Analysis unit (displayed lines and navigation facts) in one SQLite file, and answers cscope’s numbered queries from it, either one query per process or through a long-running session that editors talk to.

This page is the map. Details live in:

Stack

ConcernChoice
LanguageRust (2024 edition, stable toolchain), unsafe_code = "forbid"
Reading CRipscope’s own scanner, matched to the pinned cscope (ADR 0004)
StorageSQLite via rusqlite, bundled, in WAL mode
Matchingregex (byte-oriented, cscope’s pattern grammar translated)
Change detectionfile metadata and identity, sha2 hashes, and a session’s watch through inotify on Linux and notify on Windows
Platformrustix on Linux, winx and notify on Windows
Interfacescscope line protocol, JSON (serde_json), Vim, Neovim (Lua), Emacs (Lisp)

Every dependency is pinned to an exact version and the lockfile is enforced with --locked.

Components

The command line sends builds through Refresh and the scanner into one SQLite index. Stored queries read one index revision, and text queries read current source files. Both return rows through the result sink. A session's watch learns from each Refresh what it reads and tells the session when the source files changed.
ModuleResponsibility
invocationReads a command line the way cscope reads its options, and refuses what Ripscope cannot honour.
cli::pipelineIndex builds, --verify, one-shot queries and sessions. Frames answers, prompts and requests that cannot be answered.
refreshApplies the selection rule, detects changed files, analyses them and follows #includes, then publishes one revision.
refresh::includesOwns effective include search directories and lookup caches for one Refresh, and records what it asked the file system so a later Refresh can ask again.
watchA session’s Watch: covers what each Refresh reads before it reads it (inotify on Linux, ReadDirectoryChangesW on Windows) and tells the session before each query whether a Refresh could find anything changed (ADR 0008).
analyzerThe Analyzer: reads a captured source into its Analysis unit with a set of Corrections, and names itself, so an index knows which analyses it holds.
scannerTurns a file’s bytes into cscope’s line records. Corrections are switchable readings at named decision points.
unitThe Analysis unit: a file’s displayed lines and ordered navigation events, as one validated image.
storeThe one SQLite file: selection rule, files in database order, shared units, include operands.
revisionThe index revision a query reads: the indexed files in database order and their Analysis units, read from the index in one transaction or made in memory for the compass and tests. It is the one place queries read a revision.
queryCompiles a query field’s pattern with the product’s limits, as a stored query or a live text query, and gives a stored query’s Result rows with their context column.
engineAnswers fields 0, 1, 2, 3, 7, 8 and 9 over Analysis units with Ripscope’s own query semantics, and keeps cscope’s sequential search as the oracle.
sinkEncodes, counts and spools result rows in their output order, and places answer warnings for each reader.

Updating an index

An index build (-b), a query without -d, a session’s r and an updating session’s query after its Watch reports a change all bring the index up to date the same way: one SQLite transaction that holds the write lock from start to commit. A Refresh tells the Watch its rule and starting revision before it reads any file, and what the revision it leaves holds.

After applying file-list options, Refresh constructs one private Include resolver. Discovery returns canonical indexed spellings in cscope’s build-root and include-directory order, stopping at already-listed paths. It asks the file system only whether a candidate is a regular file spelled exactly (through directory listings on Windows), and its caches start fresh for each Refresh. Each file is read from its own bytes, without the headers it includes (ADR 0003, “Each file is read as written, without a compiler or preprocessor”), so only a file whose bytes changed is analysed again.

Refresh holds one write transaction, keeps unchanged Analysis units, scans changed sources and stores their units before committing. Readers retain their existing revision until their next query.

Readers keep the revision they started with while an update runs, and a session sees the new one at its next query.

Answering a query

Queries update first unless -d is set. Stored fields read Analysis units and text fields read current source files. The result sink formats either cscope rows or JSON.

The result sink spools answers, so their row count and any warnings are known before the first row is written. It keeps rows at file scope before the other rows, including when several threads answer parts of a query. Each of its two streams keeps up to 8 MiB in memory for a complete answer, or 1 MiB for a part, then spills to a temporary file.

The transport supplies the result header. The sink writes answer warnings to stderr before that header for line sessions and one-shot queries (including one-shot JSON), or as warning frames after the rows in JSON sessions. Vim’s short notice and RETURN acknowledgement remain session framing. A failure while finding or spooling rows delivers no partial answer, and a failure while writing the finished answer stops delivery.

Where to look in the code

QuestionStart at
How arguments are readsrc/invocation.rs
Index builds, queries, sessionssrc/cli/pipeline.rs, src/cli/pipeline/
Reading Csrc/scanner/
The index filesrc/store.rs
Updates and the include closuresrc/refresh.rs
Query fieldssrc/query.rs, src/engine.rs, src/query/pattern.rs
What a query reads from the indexReads in src/engine.rs, src/revision.rs
Outputsrc/sink.rs