103 Commits
Author SHA1 Message Date
Stefan HallerandClaude Fable 5.1 c7f62ea9b6 Keep the topmost directory selected when a compressed directory splits
When a single file is modified inside a nested directory, the file tree
compresses the whole chain of directories into one line, such as
"pkg/gui/controllers/helpers". Selecting that line shows the diff of the
entire working tree. When a second file is then modified in another
subdirectory of pkg/gui, the tree splits the line into "pkg/gui" with
"context" and "controllers/helpers" below it, and the refresh moves the
selection down to "controllers/helpers". Users who keep the top
directory selected to see the diff of everything lose that view and
have to move the cursor back up after every such refresh.

This happens because the selection is re-found by the node's own path,
and a compressed node's path is the deepest directory in its chain. The
node stood for every directory in that chain, though, and the topmost
piece of the split is the one that stays on the same line.

Match a compressed directory node against any new node that stands for
at least one of the same directories. The list is in depth-first order,
so the topmost piece wins and the cursor stays on its line. Files are
never compressed, so their handling doesn't change. The reverse case,
where two directories fold back into one compressed line, already
selected the merged line and still does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:04:57 +02:00
Stefan HallerandClaude Fable 5.1 7d576c3d30 Expand to the new half of a rename after rebuilding the tree
When the deletion of the selected file is staged and git then reports
it as the old half of a rename whose new half sits in a collapsed
directory, the selection is meant to move to the rename. With
gui.showRootItemInFileTree turned on, the directory stays collapsed and
the selection lands on it. With the option turned off, the directory
expands, but if another file in it sorts before the rename, that file
gets selected.

The loop that expands the directory runs before the tree is rebuilt and
works on the file list instead of on tree nodes. It compares the
rename's previous path, a user-facing path, against the selected node's
internal path, so the two never match while the root item is shown. The
path it hands to ExpandToPath is user-facing as well, so the directory
would stay collapsed either way. And because the loop runs before the
old node list is captured, expanding a directory above the selection
shifts that list, and the search for the new selection starts from
whatever node moved into the selected line.

Rebuild the tree first, capture the old node list before anything
expands, and then look for the rename among the leaves of the new tree.
ExpandToPath gets the leaf's own internal path, so the two kinds of
paths never need converting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:03:49 +02:00
Stefan HallerandClaude Fable 5.1 b4b6a993ea Add a test for following a file into a rename in a collapsed directory
When the deletion of the selected file is staged and git then reports
it as the old half of a rename whose new half sits in a collapsed
directory, the selection is meant to move to the rename, expanding the
directory on the way. With gui.showRootItemInFileTree turned on, the
directory stays collapsed and the selection lands on it instead. With
the option turned off, the directory does expand, but if another file
in it sorts before the rename, that file gets selected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:03:05 +02:00
Stefan HallerandClaude Fable 5.1 4766ce009e Compare user-facing paths when re-finding the selection after a refresh
When the selected rename splits into its two halves, e.g. because it was
unstaged, the selection is meant to move to the new half. With the
default setting of gui.showRootItemInFileTree, it lands on the file that
follows the rename in the list instead.

findNewSelectedIdx identifies a rename by the names of its two halves.
These are user-facing paths, without the "./" prefix that the root item
adds to every internal path, but they were compared against internal
paths. The comparison never matched while the root item was shown.

Compare user-facing paths throughout findNewSelectedIdx. Node.ID already
identifies list items by their user-facing path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:32:33 +02:00
Stefan HallerandClaude Fable 5.1 77cbb532f2 Add a test for the selection after a selected rename splits in two
When a rename is selected in the files panel and then splits into its
two halves, e.g. because it was unstaged, the selection is meant to move
to the new half. This only works with gui.showRootItemInFileTree turned
off. With the default setting, the selection lands on the file that
follows the rename in the list instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:31:38 +02:00
Stefan HallerandClaude Opus 5 bb7e74968b Drop the menu-specific wording for the filter prompt
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>
2026-08-31 20:41:22 +02:00
Stefan HallerandClaude Opus 5 43b47d16dd Keep showing files whose conflicts have been resolved
When several files have conflicts, resolving one of them makes it vanish
from the files panel as soon as it is auto-staged, and it only comes back
once the last conflict is resolved and the filter turns off again. By
then it sits among all the other changed files of the merge, so it is
hard to find the ones whose resulting diff you still wanted to check.

So remember which files had conflicts while the conflicted-files filter
is on, and keep showing them once they are resolved. This is the general
solution that 39513d244d called for; that commit only helped for the
case of a single conflicted file.

The consequence is that the selection no longer moves on to the next
conflicted file when one is resolved: it stays on the file you just
resolved, which shows you its diff right away.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 16:45:50 +02:00
Stefan Haller 4e2a1cd5b0 Add a function SetStatusFilterPreservingSelection 2026-08-15 14:58:27 +02:00
Stefan Haller b2b9519bcc Extract a private helper function preserveSelection
The operation around which the selection should be preserved is passed
in so that it can be reused for different purposes.
2026-08-15 14:58:10 +02:00
Stefan Haller bc9fafff02 Make the conflict marker size a parameter of our marker matching
Git doesn't always write conflict markers of seven characters: the
conflict-marker-size gitattribute overrides that per file, and it is set
for good reasons — for file types whose regular content tends to contain
marker-looking lines, such as documentation about merging, or test
scripts. We hard-code seven characters everywhere we look for markers,
so none of that works.

Prepare for honoring the attribute by threading the marker size through
everything that recognizes a marker, carried on the file model. Nothing
fills it in yet, so we still use git's default size of seven everywhere,
and matching is unchanged: a marker consists of exactly that many marker
characters, and all but the "=======" one are followed by a space and a
label.
2026-08-08 12:43:42 +02:00
Stefan HallerandClaude Opus 4.8 7c4d8045f9 Clamp the commit-file tree selection when the tree is rebuilt
CommitFileTreeViewModel embedded the low-level tree's SetTree, which
rebuilds the node list without touching the cursor. So after a shrinking
rebuild (e.g. moving a patch out into the index removes a file), the
selection index could be left past the end of the tree. GetSelectedItems
then indexes out of range and returns a nil node, which segfaults callers
such as canEditFiles when the options map is rendered during layout.

Override SetTree to ClampSelection after the rebuild. Unlike
FileTreeViewModel we deliberately don't also re-find the selected node by
path: that walk lands on the containing directory when a file is removed
from a dir that then collapses, whereas keeping the clamped index lands
on the sibling file (see discard_old_file_changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 2c139b6ac1 Remove RefreshingFilesMutex/FileTreeViewModel.RWMutex, dead code
FileTreeViewModel.RWMutex is removed along with the
withFileTreeViewModelMutex wrapper in FilesController that RLocked it:
every writer (the bounce closure, previous commit) and every reader (key
handlers, disabled-reason callbacks) now runs on the UI thread, so the
mutex is redundant.

RefreshingFilesMutex is removed entirely, including its last use in
repos_helper's DispatchSwitchTo. That use predates the bounce and was
never about FilesController's optimistic-rendering concern; it serialized
a repo switch's onNewRepo() against an in-flight FILES refresh for the
repo being switched away from, so that a slow refresh from the old repo
couldn't write into the freshly-reset model for the new one. Bouncing the
write already broke that guarantee on its own terms — the mutex's critical
section never covered the bounced closure's actual execution, only the
(now-removed) code that enqueued it — so by this point it was only still
locked here without protecting anything real; the previous commit's
repo-generation guard is what now actually closes that race, making this
lock fully redundant rather than just relocated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan Haller 5350b6c37b Remove unused CommitFileTreeViewModel.RWMutex 2026-07-07 18:09:33 +02:00
Stefan Haller b8d23dd5a5 Make file sort order and case sensitivity configurable
Set the sort order's default from the former foldersFirst to mixed, so this is a
change in behavior. I find this useful because it now matches git's order, so if
you look at the diff of a commit, the TOC at the top has the same order as the
file tree you see when entering the commit.
2026-03-30 17:53:25 +02:00
Stefan Haller 4a8ab6f64e Cleanup: simplify by using lo.FilterMap instead of separate calls to Filter and Map 2026-03-27 14:34:47 +01:00
blakemckeany 61b72cef38 Add GetVisualDepth method to FileTree/CommitFileTree 2026-03-25 16:41:12 +01:00
Jesse DuffieldandClaude Opus 4.6 615c566ac4 Filter file views rather than search
Change working tree files and commit files panels to use filtering
(reducing the list) instead of search (highlighting matches). This
matches the behavior of other filterable views.

The text filter matches against the full file path, not just the
filename, which is more useful for navigating large directory trees.

When toggling a directory for a custom patch while a text filter is
active, only the visible filtered files in the directory are affected,
consistent with how staging a directory in the files panel works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:04:31 +01:00
Jesse DuffieldandClaude Opus 4.6 60316388c0 Rename GetFilter to GetStatusFilter on IFileTree
This avoids a naming collision with GetFilter from the
IFilterableContext interface, which will be implemented by
FileTreeViewModel in the next commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:04:31 +01:00
Stefan Haller 7fe73c1ee2 When entering a commit in path filtering mode, select the filtered path 2025-10-09 09:58:15 +02:00
Stefan Haller 5811f2945c Extract a InternalTreePathForFilePath helper function 2025-10-09 09:58:10 +02:00
Stefan Haller 4b9921d0a4 Move the Ref interface from gui/types to models
This is a type that can be useful for model/backend stuff, so move it there. We
are going to use it in the API of the commit loader.
2025-07-31 15:21:34 +02:00
Stefan Haller ca05a2ccea Enable revive linter, and fix a bunch of warnings
I took the set of enabled checks from revive's recommended configuration [1],
and removed some that I didn't like. There might be other useful checks in
revive that we might want to enable, but this is a nice improvement already.

The bulk of the changes here are removing unnecessary else statements after
returns, but there are a few others too.

[1] https://github.com/mgechev/revive?tab=readme-ov-file#recommended-configuration
2025-06-30 19:13:20 +02:00
Stefan Haller 3cff48437e Add user config gui.addRootItemInFileTree 2025-05-24 18:17:25 +02:00
Stefan Haller ffb8586795 Pass common.Common to file trees instead of just the Log
We will need a user config in the file tree in the next commit, and passing the
entire common is the easiest way to do that while ensuring hot-reloading when
users change the config while lazygit is running.
2025-05-24 17:56:43 +02:00
Brandon 1fa9ea7f04 Fix discarding submodule changes in nested folders
The current rules for discarding submodule changes is that no other changed item
must be also selected. There are some bugs with the current implementation when
submodules are in folders.

For example, selecting and discarding a folder with only a nested submodule
change will currently do nothing. The submodule changes should be discarded. The
folder only contains submodule changes so it should be no different than
pressing discard on the submodule entry itself.

Also, I noticed range selecting both the folder and the submodule and then
pressing discard would be incorrectly disallowed.
2025-04-07 09:42:19 +02:00
Brandon 106b7018dd Cleanup: rename predicate params (test -> predicate) 2025-04-07 09:39:42 +02:00
Stefan Haller 26459523f5 Add a root item ("/") if files at top level have changed 2025-03-21 08:04:58 +01:00
Stefan Haller 0b42cfb568 Make Node.path private
This is in preparation for changing the meaning of path in the next commit.
2025-03-20 12:31:34 +01:00
Stefan Haller 3f4cb8bdda Use Path directly instead of GetPath getter inside the filetree package 2025-03-20 12:31:34 +01:00
Stefan Haller 2dfc3491bd Rename Name to Path in File and CommitFile
Name was very confusing and misleading.
2025-03-20 12:31:34 +01:00
Partho Kunda 220f06052a Show staged but untracked files in tracked only filter view 2025-03-17 19:50:58 +01:00
Kevin Radloff be7583dd40 Update to go 1.24 2025-03-08 14:53:54 -05:00
Jesse Duffield 7e85cdd027 Allow user to filter the files view to only show untracked files
This handles the situation where the user's own config says to not show
untracked files, as is often the case with bare repos managing a user's
dotfiles.
2025-02-07 09:04:55 +11:00
Mauricio TrajanoandStefan Haller 7bea41534b Collapse/expand all files in tree
Co-authored-by: Stefan Haller <stefan@haller-berlin.de>
2025-01-13 21:13:11 +01:00
Yaroslav Veremenko fdeaf9cea0 Add new filter to only show tracked files in Files panel
This allows to hide all non-tracked files on large repos
2024-11-12 16:54:24 +11:00
partho.kunda b18f12ca0f Use map to quickly find children in BuildTreeFromFiles 2024-09-18 09:16:58 +02:00
Stefan Haller 1191aca60f Actually look for conflict markers in GetHasInlineMergeConflicts
So far, lazygit has always auto-staged files as soon as the conflict markers
disappeared from them, which means that we could rely on any file that still had
a status of "UU" to still contain conflict markers.

We are going to make the auto-staging optional in the next commit, and in that
case the user will want to manually stage "UU" files; so we must now check
whether the file contains conflict markers, and disallow the staging in that
case.
2024-09-02 18:12:47 +02:00
Stefan Haller ef7d1a8602 Use non-sticky range diff when entering commit files panel
We make the name of the GetSelectedRefRangeForDiffFiles very specific on purpose
to make it clear that this is only for switching to diff files, so the
implementations can make assumptions about that (unlike GetSelectedRef, which is
used for different purposes and needs to stay more generic).
2024-08-28 19:51:15 +02:00
Jesse Duffield fdff2dec79 Remove redundant variable dedeclarations
In go 1.22, loop variables are redeclared with each iteration of the
loop, rather than simple updated on each iteration. This means that we
no longer need to manually redeclare variables when they're closed over
by a function.
2024-05-19 16:38:21 +10:00
knowmost 0677a58e9f chore: fix some comments and typos
Signed-off-by: knowmost <knowmost@outlook.com>
2024-04-28 09:44:59 +02:00
Eng Zer Jun f933a2f7ec Replace min/max helpers with built-in min/max
We upgraded our minimum Go version to 1.21 in commit
57ac9c2189. We can now replace our
`utils.Min` and `utils.Max` functions with the built-in `min` and `max`.

Reference: https://go.dev/ref/spec#Min_and_max
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-04-07 23:24:10 +08:00
Aaron Hoffman 510f9a1ae1 Support selecting file range in patch builder
test: add move_range_to_index

test: add toggle_range
2024-01-28 12:00:47 +11:00
Jesse Duffield bd7fabef1f Reduce the chance of race condition with list cursor
Before this commit, we had pkg/integration/tests/submodule/add.go
failing with a panic. I'm pretty sure the issue is this: we're now
calling quite a few GetDisabledReason calls on each layout() call,
and if a background thread happens to update a model slice while
we're doing this, we can end up with a selection index that's now
out of bounds because it hasn't been clamped to match the new list
length.

Specifically, here we had the selected index being -1 (the list starts
empty and somehow the value is -1 in this case) and then the list
gets a new submodule so the length is now 1, but the list cursor
doesn't know about this so remains on the old value. Then we confirm
the length is greater than zero and try to get the selected submodule
and get an out of bounds error.

This commit fixes the issue by clamping the selected index whenever
we get the length of the list so that it stays in-sync. This is not
a perfect solution because the length can change at any time, but
it seems to reliably fix the test, and using mutexes didn't seem to
make a difference.

Note that we're swapping the order of IFileTree and IListCursor in
the file tree view model to ensure that the list cursor's Len()
method is called (which performs the clamping).

Also, comment from the PR:
This 'trait' pattern we're using is convenient but can lead to awkward
situations. In this case we have both the list view model and the
(embedded) list cursor with a Len() method. The list cursor Len()
method just calls the list view model Len() method. But I wanted
to make it that the list view model now calls ClampSelection() on the
list cursor whenever it obtains the length. This will cause an
infinite loop because ClampSelection() internally calls Len()
(which calls the list view model's Len() method which in turn
calls ClampSelection() again, etc).

The only reason we were passing the list view model into the list
cursor was to supply the length method, so now we're just doing
that directly, and letting the list view model delegate the Len()
call to the list cursor, which now itself calls ClampSelection.
2024-01-28 09:20:52 +11:00
Jesse Duffield c07b3fad64 Ensure file view length is never returned as -1
This was causing issues when obtaining selected items
2024-01-28 08:33:13 +11:00
Jesse Duffield 269ef7f250 Support range select for staging/discarding files
As part of this, you must now press enter on a merge conflict file
to focus the merge view; you can no longer press space and if you do
it will raise an error.
2024-01-25 11:34:59 +11:00
Jesse Duffield a5f3515ad8 Set groundwork for better disabled reasons with range select
Something dumb that we're currently doing is expecting list items
to define an ID method which returns a string. We use that when copying
items to clipboard with ctrl+o and when getting a ref name for diffing.

This commit gets us a little deeper into that hole by explicitly requiring
list items to implement that method so that we can easily use the new
helper functions in list_controller_trait.go.

In future we need to just remove the whole ID thing entirely but I'm too
lazy to do that right now.
2024-01-23 13:03:37 +11:00
Jesse Duffield 54bd94ad24 Add SetSelection function for list contexts and use it in most places
The only time we should call SetSelectedLineIdx is when we are happy for a
select range to be retained which means things like moving the selected line
index to top top/bottom or up/down a page as the user navigates.

But in every other case we should now call SetSelection because that will
set the selected index and cancel the range which is almost always what we
want.
2024-01-19 10:47:21 +11:00
Jesse Duffield 24a4302c52 Add range selection ability on list contexts
This adds range select ability in two ways:
1) Sticky: like what we already have with the staging view i.e. press v then use arrow keys
2) Non-sticky: where you just use shift+up/down to expand the range

The state machine works like this:
(no range, press 'v') -> sticky range
(no range, press arrow) -> no range
(no range, press shift+arrow) -> nonsticky range
(sticky range, press 'v') -> no range
(sticky range, press arrow) -> sticky range
(sticky range, press shift+arrow) -> nonsticky range
(nonsticky range, press 'v') -> no range
(nonsticky range, press arrow) -> no range
(nonsticky range, press shift+arrow) -> nonsticky range
2024-01-19 10:47:21 +11:00
Stefan Haller f99c59b6d5 Fall back to WithWaitingStatus if item is scrolled out of view 2023-12-10 16:03:25 +01:00
AzraelSec c7012528fc feat: introduce a copy menu into the file view 2023-12-07 08:30:03 +01:00