Running the tests in an exported source tarball fails with "must run in
lazy project folder or child folder". GetLazyRootDirectory searches the
working directory and its parents for a .git directory, and a tarball
doesn't have one. This has always affected the integration tests; since
34da956f5d a unit test calls the function too, so now even
`go test ./... -short` fails.
Search for the go.mod file that declares lazygit's module instead. It
ships in tarballs, and there is exactly one of it per source tree.
Put the function in our own pkg/utils rather than change lazycore's; the
criterion is specific to lazygit, and I don't feel like making a change
to lazycore.
Return an error rather than call log.Fatal, and report it from the two
callers that run under `go test`. In a test binary, log.Fatal exits
without attributing the failure to any test. That is the failure mode
34da956f5d set out to remove. The remaining callers are development
tools that have nothing useful to do without the root directory; they
keep exiting, now through MustFindLazygitRootDirectory.
Also stop the search at the root of the file system rather than at "/".
On Windows the old loop walks up to "C:\" and then spins there forever.
A refresh left the focused main view alone while a search was on, so the
diff on screen stayed as it was however much the working tree had moved
on underneath it. The search could not cope with the content changing
under it, and leaving the content alone was the way around that.
It can cope now. The positions are worked out again from whatever the
view holds, and the status with them, so render it like any other.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rendering a view's content again while a search is on leaves the "x of y"
describing the content that has just been replaced. The status is worked
out when the search is typed and again when a key steps through the
matches, and a render is neither. Change the diff context size while
searching the focused main view, and the count stays as it was, however
many matches the wider context brought in or took away.
Run the search again over the new content once the render has finished
putting it there.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening the search prompt reads the whole of the view's content, so that
the search counts every match in it. Rendering the content again reads
only as much as the scrollbar needs, so the matches below that point are
lost. The "x of y" drops to what the shortened content holds, and grows
again as the user scrolls far enough to load more.
Read to the end while a search is on, the way opening the prompt does.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A view's search positions were worked out again from every write, and
each of those walks the whole view. Content arrives a line at a time, so
rendering into a searched view costs a walk per line. Streaming 2000
lines takes 565ms, where the same render into an unsearched view takes
about 10ms.
Mark the positions stale on a write instead, and work them out where they
are read: when the view is drawn, when a key steps through the matches,
when the status is asked for. That is at most once a frame, and the same
2000 lines now take 8ms.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Search a view, step to the last match, then have the view re-rendered
with fewer matches in it, and the status reads "3 of 1". The positions
are worked out again whenever the content changes, but the index into
them stays where it was. Stepping on from there indexes the positions
out of range and panics.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ReadToEnd reads the rest of a view's content on the render task's own
goroutine, and calls back once it has. Nothing held a task for that, so
lazygit counted as idle from the moment the caller returned until the
callback ran. The search prompt in the focused main view opens from such
a callback, so an integration test takes the idle report as its cue to
carry on, and presses its next key while the prompt is not open yet.
Hold the task in ReadToEnd rather than in the caller, so that every
caller is covered (see docs/dev/Busy.md).
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caller of ReadLines or ReadToEnd is told that the content it asked for
has been read by the request's Then being called. A task that reaches the
end of its input answers the requests still queued behind the one it was
serving, but a task that is stopped drops them, and their callers wait
for a callback that never comes. Pressing "/" in the focused main view
opens the search prompt from such a callback, so if a re-render replaces
the task at that moment the prompt never opens.
Answering them as the read loop ends would leave a request handed over
after that point unanswered, and there is a window for one. A caller
reads the channel to send on, and can reach the send itself only once the
loop has gone. So hand requests over through a queue instead. Asking
whether a task is there and giving it the request are one step, as are
taking the task away and handing back what it never answered; a request
made in between goes back to the caller to answer.
The queue is unbounded rather than a fixed-size channel, for the reasons
gocui's userEventQueue is. Requests are handed over from the UI thread,
where a blocking send would deadlock against the task waiting to be let
go, and a fixed channel that fills up leaves only blocking, dropping,
reordering or panicking to choose between.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ask a view buffer manager to read to the end of its content twice over,
then stop the task before it has served either request, as a re-render
replacing it does. Only the request it had already picked up is answered;
the one still queued behind it is dropped, and its caller waits for a
callback that never comes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two places nudged the flags because nothing else would: switching repos,
where the view focused in the repo being left is not the one focused in the
repo being entered, and tabbing from the suggestions list back to the
prompt, which replaces the top of the stack rather than popping it, so the
suggestions context never hears that it lost the focus. Both are just a
context leaving the stack now.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A view drew a selection because something told it to, from four places on
three different schedules: a context being focused, a context losing focus,
a context being activated over another one, and a list being re-rendered.
Whether the flags ended up describing the state of the app depended on
which of those had run last, and the last one to run was often none of
them: a refresh only re-focuses the view that has the focus, so a list
whose contents changed underneath an unfocused panel kept whichever
highlight it happened to have.
Derive both flags instead, in one place, from the two things they mean: a
view shows a selection while its context is on the stack and has something
to select, and the context the user is in shows an active one where the
ones behind it show inactive ones. Nothing else needs to say anything about
highlighting, so nothing else can leave a view saying something untrue
about where the focus is.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggling whitespace needs the panel beneath to render its diff again, which
is what HandleRenderToMain is for; HandleFocus does that and also everything
else that belongs to a panel gaining the focus, which this panel already has
or, when the focus is in the main view, does not want. Re-selecting its
current item is harmless, but re-deriving its highlight as a focused panel's
is not: the selection turns bright while the user is somewhere else.
Changing the context size and switching diff renderers already ask for a
re-render this way.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggling whitespace re-focuses the side panel to re-render the diff, which
also re-derives that panel's highlight — as though the panel had the focus,
which it doesn't.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A refresh only re-derives the highlight of the view that has the focus, so
a list whose contents change while the user is somewhere else keeps the
selection it had: none for a list that just got its first item, and one
over nothing for a list that just lost its last.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whether a view draws a selection is about to be derived in one place from
the context stack, which needs to ask any context — list or not — whether
there is something for a selection to sit on. Name the existing flag after
that question, and let a list context answer it from its length.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing said that a context leaving the stack takes its selection with it,
which the work coming up is about to make the rule for every view. Two
places already depend on it and are held together by hand: switching repos,
where the view focused in the repo being left is not the one focused in the
repo being entered, and tabbing from the suggestions list back to the
prompt, which replaces the top of the stack rather than popping it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The selected line of a view says nothing about whether a selection is drawn
over it, or which of the two ways it is drawn in, and those are what the
tests coming up are about.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
If a custom command's "context" field contains a context name that
doesn't exist, lazygit panics when building the keybindings. This could
happen either because of a typo, or because a context is removed or
renamed in a later version. Prevent the panic by validating those names
at config load time, and rejecting the config as invalid there, like we
do for other config errors.
The gui package owns the list, but can't be imported from here, so it is
mirrored and a test over there ensures the copies stay in sync.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
There is no `reservedKeys` any more; the list of keys that menu items must
not shadow is `essentialKeys` in the function that creates the menu.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picking a repository out of that list is the other place where the menu is
a list to search rather than a set of commands, and its items have no keys
that typing could clash with.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The only menu that ever asked for it was the keybindings menu, which now
filters as you type and doesn't use the prompt at all. That leaves every
filterable context with the same prompt, so the whole hook can go, and
with it the two implementations that only existed to satisfy it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Looking up a keybinding is a search, so the menu that lists them is the
one that most wants this. Its items do have keys, but only as a reminder
of what they do outside the menu, so nothing is lost by not binding them.
The prompt in front of the input field says what '@' does. It only ever
showed up while the user was typing in the search prompt, so it could
afford to be wordy; on a row that is on screen for as long as the menu
is, it can't.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its footer, the hint in the menu's subtitle, where the row sits in
relation to the menu and the tooltip, and whether the text cursor is
showing are all things the tests for it need to look at.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keys for paging through a menu are ',' and '.' by default, and there
is no non-printable alternative for them, so a menu that filters as you
type would lose paging altogether as soon as the user typed anything. The
same goes for confirming and cancelling if those keys are configured as
printable ones.
So bind the physical keys for all of it, on top of whatever is configured,
and only where they aren't the configured keys anyway.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The filter input is where the keyboard points for as long as such a menu
is open, so that the first printable key can go straight into it. The menu
still gets every key the input doesn't take, because the input view is
embedded in the menu view, and the two are drawn as one focused panel.
Which keys the input takes changes once there is a filter: until then
printable keys still drive the menu, so that the configured navigation
keys work as usual, and afterwards they are all filter text. A menu item's
own keys are never bound in such a menu, because typing one has to reach
the filter rather than execute the item.
Escape gives up the filter and leaves the menu open; the next one closes
it. The filter prompt behind '/' is gone from these menus: the row already
does that job, and a second filter would only be confusing.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row is reserved for as long as such a menu is open, even while it is
still hidden, so that it can appear without moving the menu. That costs
two rows of the popup, which is why the screen has to be a little taller
before a menu is worth showing at all.
The prompt in front of the input field is dropped when the row gets too
narrow to type in, and the keybindings menu says what '@' does when it
still fits.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing shows or positions them yet. The row is two views because the
input field has to start after the "Filter:" prompt, and a gocui view is
a rectangle: the frame view draws the row and the prompt, the field sits
inside it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The footer is drawn on the bottom border of the list's view, which is not
always a free row: a panel that puts something else below the list shares
that border with it, and has to render the footer there instead.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A filter can come from somewhere other than the search prompt: a menu
that filters as you type has its own input field, and needs to apply what
is typed there without going through the prompt's state.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check was a single set of view names, so it also let a click move
between two different panels, e.g. from the prompt to the commit message.
List the panels instead, and require both views to be in the same one.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A view can only be drawn with the focused frame and title colors while it
is the current view, but a panel made of an outer view and an editable
field embedded in it has to look focused as a whole, whichever of the two
the keyboard is pointed at.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Printable keys are withheld from keybindings while the user is typing in
a field, so that they end up as text. Decide that from the field that has
the focus rather than from the view a binding happens to be registered
for: a field can be embedded in another view, and that view's keys must
be withheld too, or its bindings would swallow the characters.
That makes it worth honouring KeybindOnEdit, which has been documented
but ignored ever since it was introduced. A field that sets it sees
printable keys offered to the keybindings first, and still gets them if
no binding handles them, which is what lets a view keep its keys until
the field has something to type into.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a key matches several bindings of the same view, the first one wins;
when it matches several of the view's parent, the last one did.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two places test for "a character the user typed" by hand, and a third
one is about to be needed. Give the test a name.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Originally I thought we'd benefit from this change in this branch; turns
out that we didn't after all, because we changed the approach, but it's
a nice cleanup anyway, so we include it here.
Narrow rebase refreshes already have complete metadata for existing
todos. Reuse it so repeated todo moves do not spawn git show for the
entire list. New hashes still fall back to hydration.
This fixes two problems:
1. the list selection updated before the list content did, which caused
a bit of a wobble effect in the list
2. the main view would first update to a different commit's diff and
then back to the one that is being moved, resulting in a very ugly
flicker especially when moving the todo multiple times with auto repeat
When dragging a commit with auto-scrolling so that the original commit
leaves the viewport, dragging back into the view makes the original
commit snap back into view. This is a regression that was introduced by
aebf495dce.
Drag autoscrolling deliberately keeps rendering after the test action
has returned, so view bounds can change while the test goroutine
prepares its next mouse event. Snapshot the geometry on the event loop
before translating view-relative coordinates to avoid data races. This
hasn't been a problem so far, but only because we were lucky; the added
test assertions later in this branch would cause consistent race
detector failures without this fix.
A commit that moves an entire package elsewhere renames hundreds of
files, and passing every one of their old paths can push the command past
the length limit the OS imposes (~32k characters on Windows). Their
common parent directory does just as well whenever everything it holds
ends up in the diff anyway.
Deciding that needs to consider every file of the diff, not only those on
display, so the paths are now derived from the model rather than from the
tree; a status filter must not make a directory look emptier than it is.