Skip to content

A tab tells you the moment its file changes on disk #268

Description

@jamescrosswell

Context

Second slice of #133. #267 stops the overwrite, but a save-time guard is too late to be kind: you switch to a tab, start editing, git pull rewrites the file ten seconds later, and twenty minutes of work later you find out — with both versions now substantial and no good way out. Told at ten seconds, you'd have stopped and picked a side while it cost nothing. When you learn is as much the feature as what you're told.

Depends on #267 for the recorded mtime/length/content state.

Acceptance criteria

  • Open a file, edit it, then change it on disk from another terminal. Within about a second, and with no keypress from you, the tab shows ⚠ after the name (● EditorTab.cs ⚠) and the status bar says ⚠ EditorTab.cs changed on disk once.
  • No modal, no focus steal, no beep. You can keep typing through it. The marker is the whole of it.
  • The file's name is drawn in a warning colour in the editor tab strip and in its Explorer row, so it's visible without reading the glyph.
  • A Warning scheme is added to all four themes in src/TuiCode.Workbench/Themes/themes.json, alongside the existing Accent and Error, and both call sites read the colour from there rather than hard-coding one.
  • The marker and the colour clear when the tab is saved (including via Save stops overwriting a file that changed underneath you #267's Overwrite) or when the change is otherwise resolved.
  • A background tab gets marked too — you don't have to be looking at it. Switching to it shows the ⚠ already there.
  • Saving a ⚠ tab raises Save stops overwriting a file that changed underneath you #267's modal, as it already does.
  • git checkout between branches that don't touch the file marks nothing — events are verified by reading and compared on content.
  • Our own save doesn't mark the tab it just wrote.
  • A writer that truncates-then-rewrites (so the file is briefly empty or half-written) never leaves a ⚠ describing the partial state: events are debounced ~250 ms and then verified by reading.
  • If a watcher can't be established, the app still works. It's dropped, logged via ILogger, and nothing on screen breaks. Save stops overwriting a file that changed underneath you #267's save guard is untouched either way. (The activation-time fallback is the next slice.)
  • Open ~20 files across several directories and the session holds single-digit watchers — one non-recursive watcher per distinct open directory, not the repo tree. The last tab in a directory closing tears its watcher down.

Implementation notes

  • On Linux each FileSystemWatcher is one inotify instance and fs.inotify.max_user_instances (commonly 128) is shared with every other tool the user is running — hence per-directory, non-recursive, torn down on close. On macOS .NET's watcher is FSEvents, so there's no descriptor-per-file cost.
  • Take the watcher factory from IFileSystem's IFileSystemWatcherFactory, not new FileSystemWatcher. MockFileSystem has no built-in watcher — it throws "MockFileSystem does not have a built-in FileSystemWatcher implementation" and asks you to assign your own factory — so tests supply a fake that raises events on demand, and every rule above is testable without a real disk or a Thread.Sleep.
  • On Error (InternalBufferOverflowException) as well as on a throw at creation: drop the watcher and log. Don't retry in a loop.
  • Watcher events arrive on a background thread. Marshal with App.Invoke before touching any view, as SearchView and RevisionPickerView already do (AGENTS.md).
  • Tab titles are a TG trap: setting a Title doesn't redraw its header — clear the cached width and SetNeedsLayout(), which is exactly what EditorTab.UpdateTitle already does. Route the marker through it rather than assigning Title directly, or neighbouring headers will overlap when the width changes.
  • The status-bar slot is the one FileSaved already uses.
  • Terminal.Gui's VisualRole has no Warning, and none of the four bundled token themes set editorWarning.foreground / list.warningForeground — so the scheme genuinely is new. Pick colours that read in both the light and dark themes.

Tests

With a fake watcher factory on MockFileSystem: a change to a dirty tab marks it; a change with identical content marks nothing; our own save marks nothing; two events inside the debounce window produce one check; a half-written file read mid-event doesn't mark; a watcher that throws on creation leaves the app working and the save guard intact; a watcher Error drops that directory; closing the last tab in a directory disposes its watcher; two tabs in one directory share one watcher; a background tab is marked. Plus: every theme in themes.json defines Warning, and the tab strip and Explorer both resolve their colour from it.

Out of scope

  • Reloading anything, clean or dirty — the next slice.
  • The activation-time fallback check — the next slice.
  • ⊘ for a deleted file, Compare/Reload on the save modal, the Reload from disk command — later slices.
  • Watching the repo tree, or anything recursive.
  • Watching files that aren't open in a tab: the explorer tree's own refresh and Find results going stale are separate problems.
  • A modal, a toast, or any prompt at the moment of noticing. Deliberately not.
  • A setting to turn watching off.

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:devClaimed by the a-team Dev

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions