Skip to content

When I save a file, the gutter forgets what I haven't committed #138

Description

@jamescrosswell

Opportunity

The gutter forgets my work the moment I save it.

The gutter's change markers (#23) compare the buffer with the file as it was when you opened it or
last saved it. So they only show unsaved edits, and the tab's ● already tells you that. Save,
and every marker disappears, even though none of the work is committed. Open a file you edited
yesterday and it has no markers at all.

The question you actually have while moving around a branch is "what have I changed here that
isn't committed yet?"
Today the answer is ctr → HEAD in a separate diff tab, one file at a
time, or git diff in another terminal. That's the context switch the vision's Seeing change
theme is meant to remove. It matters most before you open a PR (#126) and when you come back to a
branch after a break.

Every editor the target user comes from answers it in the gutter:

  • VS Code: gutter indicators come from git. Since 1.100, staged changes show in a lighter
    colour, so the gutter shows everything that differs from HEAD
    (docs,
    #247395).
  • Sublime Text: Incremental Diff
    with "mini_diff": "auto" diffs files in a git repo against HEAD.
  • micro, a terminal editor: its diff plugin makes diffgutter show changes "with respect to
    the most recent Git commit rather than the diff since opening the file"
    (options).

Evidence in the code (origin/main 96f57a9):

  • EditorGutter diffs the buffer against an in-memory _baseline, taken at load and retaken by
    ResetBaseline() on Save and on Reload from disk (EditorGutter.cs:16-46, EditorTab.cs:230, 484).
    Git plays no part.
  • IGitCli.ShowFileAsync(file, rev) already runs git show rev:path for ctr
    (GitCli.cs:45), with the wrapper's 5 s timeout. A failure becomes a GitResult error, never an
    exception. GitRepository.Contains answers "is this in a repo?" without calling git.
  • Nothing watches .git/HEAD. DiskWatcher (A tab tells you the moment its file changes on disk #268) only watches the folders of open files, and
    DiskChanges reloads a clean tab by itself when its file changes on disk (A clean tab shows what is actually on disk #269), falling back to a
    check when the tab is shown if the watch couldn't be set up.

Options considered

  1. Do nothing: use ctr → HEAD. It already works and is exact. But it's a separate tab,
    one file at a time, and you have to think to ask. The gutter is on screen all day and stays
    quiet about everything you've saved.
  2. Compare the gutter with HEAD for tracked files, and with the last save everywhere else.
    ← proposed. Same markers, same colours, a better baseline. It's what VS Code, Sublime and micro
    do, so there's nothing new to learn. What you lose is "changed since my last save" in the gutter,
    but the tab's ● and cts already cover that.
  3. A setting to choose the baseline: last save or HEAD. It keeps everyone's current habit
    available. But it's a row in Settings for a choice almost nobody would make differently, and
    one more thing to test both ways. The vision asks for the smallest useful version. Easy to add
    later if anyone misses the old behaviour.
  4. Two layers: committed-vs-HEAD markers plus a separate mark for unsaved lines. This is the
    richest view, and close to VS Code's staged/unstaged shading. But it needs a second marker style
    in a one-column gutter, two diffs per keystroke, and a legend to explain. It's worth its own
    pitch once option 2 has shown whether anyone wants the finer split.
  5. Show changed files instead of changed lines, e.g. git status colours in the Explorer. That
    solves a different question ("which files?", which the Review tab already partly answers) and
    still doesn't tell you where in the file you've been.

Proposal

In a git repo, a tracked file's gutter shows what's changed since the last commit, whether or not
it's saved.

  • Open a file you changed yesterday: its uncommitted lines are marked straight away.
  • Save: the markers stay. They only go when the lines match HEAD again, whether you undo, revert,
    or commit.
  • Commit, check out or pull in another terminal pane: the markers catch up straight away, even with
    the file in front of you. A checkout that rewrites the file already reloads the tab (A clean tab shows what is actually on disk #269); a commit
    doesn't touch the file, so the editor also notices when the repo's HEAD moves.
  • Anywhere git can't answer, the gutter behaves exactly as it does today, comparing with what's on
    disk, like Sublime's Incremental Diff. That covers a file outside a repo, an untracked file, a file that's new or renamed since
    HEAD, git not installed, and a git call that fails or times out. No error, no status message:
    it's a quiet fallback, not a failure.
  • Nothing else changes: same ▎/▔ glyphs, same theme colours, same tg toggle, no new command,
    key or setting.

Mockup

WorkbenchHost.cs on a branch with three uncommitted edits, all saved. Today, after Ctrl+S,
the gutter is blank:

╭────────────────────╮
│ WorkbenchHost.cs   │
│                    ╰───────────────────────────────────────────────╮
│ 470  _commands.Register(CommandIds.Open, "Open", OpenFile);        ▲│
│ 471  _commands.Register(CommandIds.GoToLine, "Go to line…",        █│
│ 472      OpenGoToLine, CommandScope.Editor, FileOpen);             ░│
│ 473  _commands.Register(CommandIds.Recent, "Open recent", Recent); ░│
│ 474                                                                ░│
│ 475  _commands.Register(CommandIds.Blame, "Toggle blame", Blame);  ▼│
╰─────────────────────────────────────────────────────────────────────╯
 Editor  •  src/TuiCode.Workbench/WorkbenchHost.cs  •  C#          Ln 473, Col 1

Proposed, same file, same save: the uncommitted lines are still marked. The change marker and
the tinted line number are drawn exactly as they are for unsaved edits today:

╭────────────────────╮
│ WorkbenchHost.cs   │
│                    ╰───────────────────────────────────────────────╮
│ 470  _commands.Register(CommandIds.Open, "Open", OpenFile);        ▲│
│ 471▎ _commands.Register(CommandIds.GoToLine, "Go to line…",        █│
│ 472▎     OpenGoToLine, CommandScope.Editor, FileOpen);             ░│
│ 473▎ _commands.Register(CommandIds.Recent, "Open recent", Recent); ░│
│ 474▔                                                               ░│
│ 475▎ _commands.Register(CommandIds.Blame, "Toggle blame", Blame);  ▼│
╰─────────────────────────────────────────────────────────────────────╯
 Editor  •  src/TuiCode.Workbench/WorkbenchHost.cs  •  C#          Ln 473, Col 1

(471–472 modified, 473 and 475 added, a line deleted above 475.) After git commit in another
terminal pane, the gutter goes blank again by itself, as it does today after a save.

Scope

In

  • The gutter baseline for a tracked file in a repo is the file's content at HEAD, read in the
    background when the file opens, so opening a file is no slower. Markers appear once it arrives.
  • It's re-read on Save and on Reload from disk, including the automatic reload when a checkout
    rewrites an open file.
  • It's re-read when the repo's HEAD moves (commit, checkout, pull, reset, rebase outside TuiCode),
    noticed the same way open files are (A tab tells you the moment its file changes on disk #268). Where that watch can't be set up, it's re-read when the
    tab is shown instead, the same fallback DiskChanges uses.
  • HEAD's text is turned into lines the way the working copy is, so line endings and .gitattributes
    conversions don't mark every line as changed. See Rough breakdown for the Dev's note.
  • The quiet fallback to the last save in every case git can't answer, as above.
  • AGENTS.md § Editor gutter: what the baseline is now.

Out

  • Distinguishing staged from unstaged changes (VS Code's lighter shade). That's option 4.
  • A setting to pick the baseline. That's option 3.
  • Jumping between gutter changes in the editor, reverting a change from the gutter, or clicking the
    gutter to see the old text. Each is a natural next pitch once the markers mean "uncommitted".
  • Git status in the Explorer.
  • Any change to ctr, cts or the Review tab.

Rough breakdown

  1. A saved file still shows what I haven't committed. Tracked files take their baseline from
    HEAD, read in the background on open, save and reload, with the quiet fallback everywhere else.
    You notice it the first time you save and the markers stay, and when you open yesterday's file.
    Dev's note: git show HEAD:path returns the raw blob, so CRLF working copies (core.autocrlf)
    and other checkout filters would differ on every line. git cat-file --filters HEAD:path applies
    the same conversion as a checkout. Also, LineDiff gives up past 500 edits and marks the whole
    region as modified. A branch's worth of changes reaches that far more often than one editing
    session does, so the Dev should check how it looks on a heavily changed file.
  2. The markers catch up after I commit somewhere else. Watch the repo's HEAD and re-read the
    baseline for that repo's tabs when it moves, so committing, checking out or pulling in another
    pane clears or moves the markers while you look. Dev's note: in a worktree .git is a file, and the
    branch ref lives in the common git dir (git rev-parse --git-dir --git-common-dir); a ref can also
    move inside packed-refs. Fall back to re-reading when the tab is shown where the watch fails.
    Without this slice, a checkout that rewrites the file still refreshes (slice 1, via the reload),
    but a commit leaves the markers stale until the next save.

Decided

  1. Untracked files compare with what's on disk, as today, like Sublime's Incremental Diff.
    Stakeholder's answer.
  2. The markers update while you're looking, using the disk-change events the editor already has,
    plus a watch on HEAD for commits, which don't touch the file. Stakeholder's answer; this replaces
    my "tab-shown only" recommendation and makes slice 2 a watch.
  3. No vs HEAD indicator. Stakeholder's answer.

Open questions

None.

Original idea

Problem

The gutter marks added, modified and deleted lines, but only against the file as it was when you
last opened or saved it (#23). Save once and the markers disappear, even though none of that work
is committed. You can't glance at a file and see "what have I changed on this branch so far?".
When you open a file you edited yesterday, it shows no markers at all. So you check git diff in
another tab, which is exactly the context switch the vision wants to remove.

Evidence

  • VS Code: editor gutter indicators
    show changes against git, not against the last save.
  • Sublime Text: Incremental Diff with
    "mini_diff": "auto" diffs files in a git repo against HEAD.
  • micro, a terminal editor: its diff plugin
    makes diffgutter show changes "with respect to the most recent Git commit rather than the diff
    since opening the file".
  • TuiCode already has the pieces. EditorGutter diffs the buffer against a _baseline
    (LineDiff.Compute), and Compare to command #61 plans an IGitCli with show rev:path. Setting the baseline to
    git show HEAD:<path> for tracked files is mostly wiring. It would keep the save baseline for
    untracked files, outside a repo, or when git is missing.

Why it fits

  • Seeing change (theme 2): the cheapest always-on view of change, a direct follow-on to Compare to command #61's
    git wrapper, and useful as soon as it lands.
  • Daily driver: you see your uncommitted work at a glance while navigating, and before a PR
    review (PR Review Workflow #126).
  • Uses the git CLI, like Compare to command #61, so nothing is added to the binary. It degrades to today's
    behaviour when git isn't there.
  • Open question for a pitch: should the baseline refresh after a commit made in another tab? That
    overlaps with Notice when open files change on disk #133 (noticing on-disk changes).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    a-team:ideaFound by the a-team Lead; give it a Priority to have it pitchedpitchAn a-team pitch: Lead shapes it, reviewer approves it

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions