# How tuicr Implements Gap Expansion for Viewing Large Diffs

> Learn how tuicr uses gap expansion to efficiently view large diffs. Discover its three-layer architecture for seamless context handling.

- Repository: [Almog Gavra/tuicr](https://github.com/agavra/tuicr)
- Tags: internals
- Published: 2026-08-07

---

**tuicr collapses unchanged lines between diff hunks into virtual gaps and automatically expands minimal context when users jump to hidden lines, using a three-layer architecture of gap bookkeeping, expansion planning, and source-agnostic context fetching.**

tuicr is a terminal-based code review tool designed to handle massive diffs efficiently. When viewing changes, the tuicr gap expansion system hides unchanged context between hunks while maintaining the ability to instantly reveal specific lines when you navigate to them, regardless of whether the source is a local Git repository or a remote pull request.

## Gap Bookkeeping and Gap Identification

Every diff file in tuicr is represented as a `DiffFile` containing a list of hunks. The unchanged lines between these hunks—or before the first hunk and after the last—are tracked as **gaps**. Each gap receives a unique `GapId` and is categorized as either a standard inter-hunk gap or an EOF gap.

The `gap_size` function in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs) (line 4) returns the number of hidden lines for any given gap:

```rust
pub(crate) fn gap_size(&self, gap_id: &GapId) -> Option<u32> { … }

```

This function distinguishes EOF gaps from normal gaps and delegates the actual calculation to `calculate_gap` in the Git backend at [`src/vcs/git/context.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/git/context.rs) (lines 84‑99). The bookkeeping layer tracks metadata including the gap's position relative to hunks and whether it represents the end of the file.

## Locating the Target Gap

When a user jumps to a specific line number, tuicr must first determine which gap contains that line. The `find_gap_containing_lineno` function (lines 35‑86 in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs)) iterates through all hunks to compute the line range of each gap on the requested side (`LineSide::New` or `LineSide::Old`).

```rust
pub(in crate::app) fn find_gap_containing_lineno(
    &self, file_idx: usize, target_lineno: u32, side: LineSide,
) -> Option<GapId> { … }

```

The function calculates gap boundaries by comparing consecutive hunks and checking if the target line number falls within the unchanged region. It also handles the special case of the EOF gap separately (lines 64‑85), ensuring that navigation to any line in the file resolves to the correct expandable region.

## Planning the Expansion Strategy

Once tuicr identifies the containing gap, `expand_plan_to_reach` (lines 98‑159 in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs)) determines exactly how to expand the gap to reveal the target line. This planning phase considers several factors:

- **Gap type detection**: It first checks if the gap is an EOF gap (lines 107‑124), which requires different boundary handling.
- **Side translation**: For old-side targets, it calculates the equivalent new-side line number using the constant offset `new - old` that persists across unchanged context (lines 138‑142).
- **Direction selection**: Based on the cursor position relative to the gap (`cursor_below_gap`), it chooses `ExpandDirection::Up` when the cursor is below the gap, otherwise `Down` (lines 44‑58).
- **Line limit calculation**: It computes the exact number of lines needed to reach the target, preventing over-fetching.

```rust
pub(in crate::app) fn expand_plan_to_reach(
    &self, gap_id: &GapId, target_lineno: u32, side: LineSide,
) -> (ExpandDirection, Option<usize>) { … }

```

## Fetching Context from Any Source

The actual expansion is executed by `expand_gap` (lines 26‑119 in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs)), which abstracts the content source through the `ContextProvider` trait defined in [`src/forge/context.rs`](https://github.com/agavra/tuicr/blob/main/src/forge/context.rs) (lines 20‑44). This trait enables tuicr to fetch lines from both local VCS and remote forges using a unified interface:

```rust
pub trait ContextProvider {
    fn fetch_context_lines(
        &self,
        old_path: Option<&PathBuf>,
        new_path: Option<&PathBuf>,
        file_status: FileStatus,
        start_line: u32,
        end_line: u32,
    ) -> Result<Vec<DiffLine>>;

    fn file_line_count(
        &self,
        old_path: Option<&PathBuf>,
        new_path: Option<&PathBuf>,
        file_status: FileStatus,
    ) -> Result<u32>;
}

```

Two implementations handle different data sources:

- **`VcsContextProvider`** (lines 46‑91): Forwards requests to the local `VcsBackend`, optionally reading from a specific commit via `ref_commit`.
- **`ForgeContextProvider`** (lines 93‑117): Constructs `ForgeFileLinesRequest` objects to fetch content from remote PR base or head commits.

The provider selection logic in `context_provider` (lines 322‑360) automatically chooses the appropriate backend based on whether you're reviewing a local change or a remote PR.

For Git repositories, the concrete implementation in [`src/vcs/git/context.rs`](https://github.com/agavra/tuicr/blob/main/src/vcs/git/context.rs) (lines 10‑19) handles working tree files, specific commits, or HEAD blobs for deleted files, then slices the requested range using `slice_context_lines`.

## Executing the Gap Expansion

The `expand_gap` function performs the actual UI update after fetching context lines. It first populates the file line-count cache (line 34), then retrieves absolute boundaries via `gap_boundaries` (lines 75‑108). It calculates the delta between new-side and old-side line numbers (lines 57‑69) to ensure proper alignment when displaying lines.

After fetching via the `ContextProvider`, the function inserts lines according to the expansion direction:

- **Down**: Prepends fetched lines to `expanded_top` (lines 92‑97).
- **Up**: Prepends to `expanded_bottom` (lines 99‑107).
- **Both**: Fetches all remaining context (lines 109‑115).

Finally, `rebuild_annotations` (line 118) regenerates the rendering data, causing the UI to instantly display the newly revealed context without reloading the entire diff.

## Collapsing and Resetting Gaps

Users can collapse previously expanded gaps to restore the compact view. The `collapse_gap` function (lines 62‑66) removes stored expansions for a specific `GapId`:

```rust
pub fn collapse_gap(&mut self, gap_id: GapId) { … }

```

When the diff is reloaded—such as when refreshing from the remote forge—`clear_expanded_gaps` (lines 69‑73) wipes all gap expansion data, ensuring the view returns to the initial collapsed state.

## Practical Usage Examples

**Programmatically expanding a gap to reveal a specific line:**

```rust
// Navigate to line 120 in the first file on the new side
let file_idx = 0;
let target_line = 120;
let side = LineSide::New;

if let Some(gap_id) = app.find_gap_containing_lineno(file_idx, target_line, side) {
    let (direction, limit) = app.expand_plan_to_reach(&gap_id, target_line, side);
    app.expand_gap(gap_id, direction, limit).expect("expansion failed");
}

```

**Fetching context directly via the provider interface:**

```rust
let provider = app.context_provider();
let lines = provider
    .fetch_context_lines(
        Some(&old_path),
        Some(&new_path),
        FileStatus::Modified,
        200,  // start line
        210,  // end line
    )
    .expect("fetch failed");

```

**Collapsing a gap programmatically:**

```rust
app.collapse_gap(gap_id);

```

## Summary

- tuicr represents unchanged lines between hunks as **gaps** tracked by `GapId` in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs), distinguishing between standard gaps and EOF gaps.
- **Gap location** is determined by `find_gap_containing_lineno`, which maps line numbers to specific gaps based on `LineSide` (New or Old).
- **Expansion planning** via `expand_plan_to_reach` calculates the optimal direction (Up/Down) and exact line count needed to reveal a target line without over-fetching.
- The **ContextProvider trait** abstracts content fetching across `VcsContextProvider` (local Git) and `ForgeContextProvider` (remote APIs), enabling seamless gap expansion regardless of data source.
- **Execution** occurs in `expand_gap`, which fetches boundaries, calculates line number deltas, inserts lines into `expanded_top` or `expanded_bottom`, and calls `rebuild_annotations` to update the UI.
- **State management** functions `collapse_gap` and `clear_expanded_gaps` allow users to restore compact views or reset state during diff reloads.

## Frequently Asked Questions

### How does tuicr determine which direction to expand a gap?

tuicr inspects the cursor position relative to the target gap using the `cursor_below_gap` logic in `expand_plan_to_reach` (lines 44‑58). If the cursor is already positioned below the gap, it expands **Up** toward the cursor; otherwise, it expands **Down**. This minimizes the amount of content fetched while ensuring the target line becomes visible in the most intuitive direction.

### Can tuicr expand gaps when reviewing remote PRs without cloning the repository?

Yes. When reviewing remote PRs, tuicr uses `ForgeContextProvider` (defined in [`src/forge/context.rs`](https://github.com/agavra/tuicr/blob/main/src/forge/context.rs) lines 93‑117) to fetch context lines via the forge's API. This provider constructs `ForgeFileLinesRequest` objects that specify the PR's base and head SHAs, allowing gap expansion to work entirely through remote API calls without local repository access.

### What happens to expanded gaps when the diff is reloaded?

When the diff reloads—such as when refreshing to get the latest PR updates—the `clear_expanded_gaps` function (lines 69‑73 in [`src/app/gaps.rs`](https://github.com/agavra/tuicr/blob/main/src/app/gaps.rs)) wipes all expansion state. This returns the view to the initial collapsed state, ensuring that stale context is not displayed after the underlying diff changes.

### How does tuicr handle line number mapping between old and new file versions?

During expansion, tuicr calculates a **delta** between new-side and old-side line numbers using the constant offset that exists across unchanged context gaps (lines 57‑69 in `expand_gap`). When fetching lines via `ContextProvider`, it applies this delta to adjust `old_lineno` on each returned `DiffLine` (lines 81‑84), ensuring that line numbers remain synchronized regardless of which side (old or new) initiated the expansion.