Compare commits

..
Author SHA1 Message Date
Stefan Haller 781eb61ed2 Add a "Show full diff" checkbox to the filtering menu
It is only visible when filtering by path.
2025-05-25 15:22:34 +02:00
Stefan Haller b5d97570f1 Add key bindings to filtering menu 2025-05-25 15:08:15 +02:00
49 changed files with 222 additions and 742 deletions
+1 -3
View File
@@ -11,8 +11,6 @@
* [ ] You've read through your own file changes for silly mistakes etc
<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view', and make sure the title
is suitable to be included as a bullet point in release notes (i.e. phrased from a user's point
of view).
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for examples
-->
-3
View File
@@ -190,9 +190,6 @@ gui:
# This can be toggled from within Lazygit with the '`' key, but that will not change the default.
showFileTree: true
# If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.
showRootItemInFileTree: true
# If true, show the number of lines changed per file in the Files view
showNumstatInFilesView: false
+1 -1
View File
@@ -15,7 +15,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
+2 -2
View File
@@ -194,8 +194,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412 h1:8z1CpdCy9nzdj47lSLbDbCVmR5MgXsknYsuuHpzYk5M=
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e h1:NuSXZHAUSo+dSAYArlaDpmKgxVpojuKvNcRkYJcQxkY=
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
+1 -1
View File
@@ -271,7 +271,7 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscomman
Arg(hash).
ArgIf(self.AppState.IgnoreWhitespaceInDiffView, "--ignore-all-space").
Arg(fmt.Sprintf("--find-renames=%d%%", self.AppState.RenameSimilarityThreshold)).
ArgIf(filterPath != "", "--", filterPath).
ArgIf(filterPath != "" && !self.AppState.ShowFullDiffInFilterByPathMode, "--", filterPath).
Dir(self.repoPaths.worktreePath).
ToArgv()
+5 -9
View File
@@ -96,23 +96,19 @@ func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.
cmdArgs := NewGitCmd("for-each-ref").
Arg(fmt.Sprintf("--sort=%s", sortOrder)).
Arg("--format=%(refname)").
Arg("--format=%(refname:short)").
Arg("refs/remotes").
ToArgv()
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
line = strings.TrimSpace(line)
split := strings.SplitN(line, "/", 4)
if len(split) != 4 {
return false, nil
}
remoteName := split[2]
name := split[3]
if name == "HEAD" {
split := strings.SplitN(line, "/", 2)
if len(split) != 2 {
return false, nil
}
remoteName := split[0]
name := split[1]
_, ok := remoteBranchesByRemoteName[remoteName]
if !ok {
+20 -26
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
@@ -294,10 +295,14 @@ const (
Token
)
// Whenever we're asked for a password we return a nil channel to tell the
// caller to kill the process.
// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func(CredentialType) <-chan string {
return nil
ch := make(chan string)
go func() {
ch <- "\n"
}()
return ch
}
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj *CmdObj) error {
@@ -335,7 +340,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj)
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj.GetTask())
})
})
}
@@ -344,10 +349,9 @@ func (self *cmdObjRunner) processOutput(
reader io.Reader,
writer io.Writer,
promptUserForCredential func(CredentialType) <-chan string,
cmdObj *CmdObj,
task gocui.Task,
) {
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
task := cmdObj.GetTask()
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanBytes)
@@ -356,26 +360,16 @@ func (self *cmdObjRunner) processOutput(
askFor, ok := checkForCredentialRequest(newBytes)
if ok {
responseChan := promptUserForCredential(askFor)
if responseChan == nil {
// Returning a nil channel means we should kill the process.
// Note that we don't break the loop after this, because we
// still need to drain the output, otherwise the Wait() call
// later might block.
if err := Kill(cmdObj.GetCmd()); err != nil {
self.log.Error(err)
}
} else {
if task != nil {
task.Pause()
}
toInput := <-responseChan
if task != nil {
task.Continue()
}
// If the return data is empty we don't write anything to stdin
if toInput != "" {
_, _ = writer.Write([]byte(toInput))
}
if task != nil {
task.Pause()
}
toInput := <-responseChan
if task != nil {
task.Continue()
}
// If the return data is empty we don't write anything to stdin
if toInput != "" {
_, _ = writer.Write([]byte(toInput))
}
}
}
@@ -120,8 +120,8 @@ func TestProcessOutput(t *testing.T) {
reader := strings.NewReader(scenario.output)
writer := &strings.Builder{}
cmdObj := &CmdObj{task: gocui.NewFakeTask()}
runner.processOutput(reader, writer, toChanFn(scenario.promptUserForCredential), cmdObj)
task := gocui.NewFakeTask()
runner.processOutput(reader, writer, toChanFn(scenario.promptUserForCredential), task)
if writer.String() != scenario.expectedToWrite {
t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String())
+3 -1
View File
@@ -136,7 +136,7 @@ func NewAppConfig(
}
func ConfigDir() string {
_, filePath := findConfigFile(ConfigFilename)
_, filePath := findConfigFile("config.yml")
return filepath.Dir(filePath)
}
@@ -691,6 +691,8 @@ type AppState struct {
// This determines whether the git graph is rendered in the commits panel
// One of 'always' | 'never' | 'when-maximised'
GitLogShowGraph string
ShowFullDiffInFilterByPathMode bool
}
func getDefaultAppState() *AppState {
-3
View File
@@ -123,8 +123,6 @@ type GuiConfig struct {
// If true, display the files in the file views as a tree. If false, display the files as a flat list.
// This can be toggled from within Lazygit with the '`' key, but that will not change the default.
ShowFileTree bool `yaml:"showFileTree"`
// If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.
ShowRootItemInFileTree bool `yaml:"showRootItemInFileTree"`
// If true, show the number of lines changed per file in the Files view
ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"`
// If true, show a random tip in the command log when Lazygit starts
@@ -766,7 +764,6 @@ func GetDefaultConfig() *UserConfig {
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowRootItemInFileTree: true,
ShowNumstatInFilesView: false,
ShowRandomTip: true,
ShowIcons: false,
+1 -1
View File
@@ -29,7 +29,7 @@ var (
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
viewModel := filetree.NewCommitFileTreeViewModel(
func() []*models.CommitFile { return c.Model().CommitFiles },
c.Common,
c.Log,
c.UserConfig().Gui.ShowFileTree,
)
+2 -2
View File
@@ -24,14 +24,14 @@ var (
func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
viewModel := filetree.NewFileTreeViewModel(
func() []*models.File { return c.Model().Files },
c.Common,
c.Log,
c.UserConfig().Gui.ShowFileTree,
)
getDisplayStrings := func(_ int, _ int) [][]string {
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons, c.UserConfig().Gui.ShowRootItemInFileTree)
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
@@ -361,7 +361,7 @@ func (self *BasicCommitsController) newBranch(commit *models.Commit) error {
}
func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error {
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash(), commit.Hash())
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash())
}
func (self *BasicCommitsController) checkout(commit *models.Commit) error {
+2 -3
View File
@@ -326,8 +326,7 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
LabelColumns: []string{upstreamResetOptions},
OpensMenu: true,
OnPress: func() error {
// We only can invoke this when the remote branch is stored locally, so using the selectedBranch here is fine.
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream, selectedBranch.FullUpstreamRefName())
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
if err != nil {
return err
}
@@ -687,7 +686,7 @@ func (self *BranchesController) createSortMenu() error {
}
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name, selectedBranch.FullRefName())
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name)
}
func (self *BranchesController) rename(branch *models.Branch) error {
+1 -1
View File
@@ -1144,7 +1144,7 @@ func (self *FilesController) stash() error {
}
func (self *FilesController) createResetToUpstreamMenu() error {
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}", "@{upstream}")
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}")
}
func (self *FilesController) handleToggleDirCollapsed() error {
@@ -60,6 +60,7 @@ func (self *FilteringMenuAction) Call() error {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.FilterPathOption,
Key: 'p',
OnPress: func() error {
self.c.Prompt(types.PromptOpts{
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetFilePathSuggestionsFunc(),
@@ -76,6 +77,7 @@ func (self *FilteringMenuAction) Call() error {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.FilterAuthorOption,
Key: 'a',
OnPress: func() error {
self.c.Prompt(types.PromptOpts{
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
@@ -90,9 +92,23 @@ func (self *FilteringMenuAction) Call() error {
Tooltip: tooltip,
})
if path := self.c.Modes().Filtering.GetPath(); path != "" {
menuItems = append(menuItems, &types.MenuItem{
Label: "Show full diff", // TODO: i18n (and tooltip?)
Key: 'f',
OnPress: func() error {
self.c.AppState.ShowFullDiffInFilterByPathMode = !self.c.AppState.ShowFullDiffInFilterByPathMode
self.c.SaveAppStateAndLogError()
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
},
Widget: types.MakeMenuCheckBox(self.c.AppState.ShowFullDiffInFilterByPathMode),
})
}
if self.c.Modes().Filtering.Active() {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.ExitFilterMode,
Key: 's',
OnPress: self.c.Helpers().Mode.ClearFiltering,
})
}
+1 -1
View File
@@ -57,7 +57,7 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
from, to := refRange.From, refRange.To
args := []string{from.ParentRefName(), to.RefName(), "--stat", "-p"}
args = append(args, "--")
if path := self.c.Modes().Filtering.GetPath(); path != "" {
if path := self.c.Modes().Filtering.GetPath(); path != "" && !self.c.AppState.ShowFullDiffInFilterByPathMode {
args = append(args, path)
}
cmdObj := self.c.Git().Diff.DiffCmdObj(args)
@@ -448,6 +448,8 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
self.c.Mutexes().RefreshingBranchesMutex.Lock()
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
prevSelectedBranch := self.c.Contexts().Branches.GetSelected()
reflogCommits := self.c.Model().FilteredReflogCommits
if self.c.Modes().Filtering.Active() && self.c.AppState.LocalBranchSortOrder == "recency" {
// in filter mode we filter our reflog commits to just those containing the path
@@ -482,8 +484,6 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
self.c.Log.Error(err)
}
prevSelectedBranch := self.c.Contexts().Branches.GetSelected()
self.c.Model().Branches = branches
if refreshWorktrees {
+15 -26
View File
@@ -231,7 +231,7 @@ func (self *RefsHelper) CreateSortOrderMenu(sortOptionsOrder []string, onSelecte
})
}
func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
func (self *RefsHelper) CreateGitResetMenu(ref string) error {
type strengthWithKey struct {
strength string
label string
@@ -249,7 +249,7 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
return &types.MenuItem{
LabelColumns: []string{
row.label,
style.FgRed.Sprintf("reset --%s %s", row.strength, name),
style.FgRed.Sprintf("reset --%s %s", row.strength, ref),
},
OnPress: func() error {
self.c.LogAction("Reset")
@@ -261,7 +261,7 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
})
return self.c.Menu(types.CreateMenuOptions{
Title: fmt.Sprintf("%s %s", self.c.Tr.ResetTo, name),
Title: fmt.Sprintf("%s %s", self.c.Tr.ResetTo, ref),
Items: menuItems,
})
}
@@ -325,10 +325,13 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
if suggestedBranchName == "" {
var err error
suggestedBranchName, err = self.getSuggestedBranchName()
suggestedBranchName, err = utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
})
if err != nil {
return err
}
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
}
refresh := func() error {
@@ -396,21 +399,16 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
return err
}
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) error {
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.NewBranchNameBranchOff,
map[string]string{
"branchName": baseBranchName,
},
)
suggestedBranchName, err := self.getSuggestedBranchName()
if err != nil {
return err
}
self.c.Prompt(types.PromptOpts{
Title: prompt,
InitialContent: suggestedBranchName,
Title: prompt,
HandleConfirm: func(response string) error {
self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch)
newBranchName := SanitizedBranchName(response)
@@ -419,7 +417,6 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
})
},
})
return nil
}
isMainBranch := lo.Contains(self.c.UserConfig().Git.MainBranches, currentBranch.Name)
@@ -434,7 +431,8 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
Title: self.c.Tr.MoveCommitsToNewBranch,
Prompt: prompt,
HandleConfirm: func() error {
return withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
return nil
},
})
return nil
@@ -454,15 +452,17 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
{
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName),
OnPress: func() error {
return withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
return self.moveCommitsToNewBranchOffOfMainBranch(currentBranch, newBranchName, baseBranchRef)
})
return nil
},
},
{
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchStackedItem, currentBranch.Name),
OnPress: func() error {
return withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
return nil
},
},
},
@@ -587,14 +587,3 @@ func (self *RefsHelper) ParseRemoteBranchName(fullBranchName string) (string, st
func IsSwitchBranchUncommittedChangesError(err error) bool {
return strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch")
}
func (self *RefsHelper) getSuggestedBranchName() (string, error) {
suggestedBranchName, err := utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
})
if err != nil {
return suggestedBranchName, err
}
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
return suggestedBranchName, nil
}
@@ -144,6 +144,14 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
),
Description: self.c.Tr.Pick,
Tooltip: self.c.Tr.PickCommitTooltip,
// Not displaying this because we only want to display it when a TODO commit
// is selected. A keybinding is displayed in the options view if Display is true,
// and if it's not disabled, but if we disable it whenever a non-TODO commit is
// selected, we'll be preventing pulls from happening within the commits view
// (given they both use the 'p' key). Some approaches that come to mind:
// * Allow a disabled keybinding to conditionally fallback to a global keybinding
// * Allow a separate way of deciding whether a keybinding is displayed in the options view
DisplayOnScreen: false,
},
{
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
@@ -615,7 +623,9 @@ func (self *LocalCommitsController) pick(selectedCommits []*models.Commit) error
return self.updateTodos(todo.Pick, selectedCommits)
}
panic("should be disabled when not rebasing")
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return self.pullFiles()
}
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, startIdx int, endIdx int) error {
@@ -1466,7 +1476,8 @@ func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit
}
if !self.isRebasing() {
return &types.DisabledReason{Text: self.c.Tr.PickIsOnlyAllowedDuringRebase, AllowFurtherDispatching: true}
// if not rebasing, we're going to do a pull so we don't care about the selection
return nil
}
return self.midRebaseCommandEnabled(selectedCommits, startIdx, endIdx)
@@ -158,7 +158,7 @@ func (self *RemoteBranchesController) createSortMenu() error {
}
func (self *RemoteBranchesController) createResetMenu(selectedBranch *models.RemoteBranch) error {
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName(), selectedBranch.FullRefName())
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName())
}
func (self *RemoteBranchesController) setAsUpstream(selectedBranch *models.RemoteBranch) error {
+1 -1
View File
@@ -302,7 +302,7 @@ func (self *TagsController) push(tag *models.Tag) error {
}
func (self *TagsController) createResetMenu(tag *models.Tag) error {
return self.c.Helpers().Refs.CreateGitResetMenu(tag.Name, tag.FullRefName())
return self.c.Helpers().Refs.CreateGitResetMenu(tag.Name)
}
func (self *TagsController) create() error {
+8 -16
View File
@@ -7,14 +7,14 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func BuildTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] {
func BuildTreeFromFiles(files []*models.File) *Node[models.File] {
root := &Node[models.File]{}
childrenMapsByNode := make(map[*Node[models.File]]map[string]*Node[models.File])
var curr *Node[models.File]
for _, file := range files {
splitPath := SplitFileTreePath(file.Path, showRootItem)
splitPath := split("./" + file.Path)
curr = root
outer:
for i := range splitPath {
@@ -63,19 +63,19 @@ func BuildTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.Fi
return root
}
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] {
rootAux := BuildTreeFromCommitFiles(files, showRootItem)
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
rootAux := BuildTreeFromCommitFiles(files)
sortedFiles := rootAux.GetLeaves()
return &Node[models.CommitFile]{Children: sortedFiles}
}
func BuildTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] {
func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
root := &Node[models.CommitFile]{}
var curr *Node[models.CommitFile]
for _, file := range files {
splitPath := SplitFileTreePath(file.Path, showRootItem)
splitPath := split("./" + file.Path)
curr = root
outer:
for i := range splitPath {
@@ -115,8 +115,8 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *No
return root
}
func BuildFlatTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] {
rootAux := BuildTreeFromFiles(files, showRootItem)
func BuildFlatTreeFromFiles(files []*models.File) *Node[models.File] {
rootAux := BuildTreeFromFiles(files)
sortedFiles := rootAux.GetLeaves()
// from top down we have merge conflict files, then tracked file, then untracked
@@ -160,11 +160,3 @@ func split(str string) []string {
func join(strs []string) string {
return strings.Join(strs, "/")
}
func SplitFileTreePath(path string, showRootItem bool) []string {
if showRootItem {
return split("./" + path)
}
return split(path)
}
+16 -251
View File
@@ -9,10 +9,9 @@ import (
func TestBuildTreeFromFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.File
showRootItem bool
expected *Node[models.File]
name string
files []*models.File
expected *Node[models.File]
}{
{
name: "no files",
@@ -32,7 +31,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
Path: "dir1/b",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -53,37 +51,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
},
},
},
{
name: "files in same directory, not root item",
files: []*models.File{
{
Path: "dir1/a",
},
{
Path: "dir1/b",
},
},
showRootItem: false,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
{
path: "dir1",
CompressionLevel: 0,
Children: []*Node[models.File]{
{
File: &models.File{Path: "dir1/a"},
path: "dir1/a",
},
{
File: &models.File{Path: "dir1/b"},
path: "dir1/b",
},
},
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.File{
@@ -94,7 +61,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
Path: "dir2/dir4/b",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -126,43 +92,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
},
},
},
{
name: "paths that can be compressed, no root item",
files: []*models.File{
{
Path: "dir1/dir3/a",
},
{
Path: "dir2/dir4/b",
},
},
showRootItem: false,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
{
path: "dir1/dir3",
Children: []*Node[models.File]{
{
File: &models.File{Path: "dir1/dir3/a"},
path: "dir1/dir3/a",
},
},
CompressionLevel: 1,
},
{
path: "dir2/dir4",
Children: []*Node[models.File]{
{
File: &models.File{Path: "dir2/dir4/b"},
path: "dir2/dir4/b",
},
},
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.File{
@@ -173,7 +102,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
Path: "a",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -207,7 +135,6 @@ func TestBuildTreeFromFiles(t *testing.T) {
Path: "a",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -237,7 +164,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromFiles(s.files, s.showRootItem)
result := BuildTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
@@ -245,10 +172,9 @@ func TestBuildTreeFromFiles(t *testing.T) {
func TestBuildFlatTreeFromFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.File
showRootItem bool
expected *Node[models.File]
name string
files []*models.File
expected *Node[models.File]
}{
{
name: "no files",
@@ -268,7 +194,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
Path: "dir1/b",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -285,33 +210,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
},
},
},
{
name: "files in same directory, not root item",
files: []*models.File{
{
Path: "dir1/a",
},
{
Path: "dir1/b",
},
},
showRootItem: false,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
{
File: &models.File{Path: "dir1/a"},
path: "dir1/a",
CompressionLevel: 0,
},
{
File: &models.File{Path: "dir1/b"},
path: "dir1/b",
CompressionLevel: 0,
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.File{
@@ -322,7 +220,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
Path: "dir2/b",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -339,33 +236,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
},
},
},
{
name: "paths that can be compressed, no root item",
files: []*models.File{
{
Path: "dir1/a",
},
{
Path: "dir2/b",
},
},
showRootItem: false,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
{
File: &models.File{Path: "dir1/a"},
path: "dir1/a",
CompressionLevel: 0,
},
{
File: &models.File{Path: "dir2/b"},
path: "dir2/b",
CompressionLevel: 0,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.File{
@@ -376,7 +246,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
Path: "a",
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -419,7 +288,6 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
Tracked: true,
},
},
showRootItem: true,
expected: &Node[models.File]{
path: "",
Children: []*Node[models.File]{
@@ -454,7 +322,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromFiles(s.files, s.showRootItem)
result := BuildFlatTreeFromFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
@@ -462,10 +330,9 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
func TestBuildTreeFromCommitFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.CommitFile
showRootItem bool
expected *Node[models.CommitFile]
name string
files []*models.CommitFile
expected *Node[models.CommitFile]
}{
{
name: "no files",
@@ -485,7 +352,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
Path: "dir1/b",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -506,37 +372,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
},
},
},
{
name: "files in same directory, not root item",
files: []*models.CommitFile{
{
Path: "dir1/a",
},
{
Path: "dir1/b",
},
},
showRootItem: false,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
{
path: "dir1",
CompressionLevel: 0,
Children: []*Node[models.CommitFile]{
{
File: &models.CommitFile{Path: "dir1/a"},
path: "dir1/a",
},
{
File: &models.CommitFile{Path: "dir1/b"},
path: "dir1/b",
},
},
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.CommitFile{
@@ -547,7 +382,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
Path: "dir2/dir4/b",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -579,43 +413,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
},
},
},
{
name: "paths that can be compressed, no root item",
files: []*models.CommitFile{
{
Path: "dir1/dir3/a",
},
{
Path: "dir2/dir4/b",
},
},
showRootItem: false,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
{
path: "dir1/dir3",
Children: []*Node[models.CommitFile]{
{
File: &models.CommitFile{Path: "dir1/dir3/a"},
path: "dir1/dir3/a",
},
},
CompressionLevel: 1,
},
{
path: "dir2/dir4",
Children: []*Node[models.CommitFile]{
{
File: &models.CommitFile{Path: "dir2/dir4/b"},
path: "dir2/dir4/b",
},
},
CompressionLevel: 1,
},
},
},
},
{
name: "paths that can be sorted",
files: []*models.CommitFile{
@@ -626,7 +423,6 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
Path: "a",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -650,7 +446,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := BuildTreeFromCommitFiles(s.files, s.showRootItem)
result := BuildTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
@@ -658,10 +454,9 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
scenarios := []struct {
name string
files []*models.CommitFile
showRootItem bool
expected *Node[models.CommitFile]
name string
files []*models.CommitFile
expected *Node[models.CommitFile]
}{
{
name: "no files",
@@ -681,7 +476,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
Path: "dir1/b",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -698,33 +492,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
},
},
},
{
name: "files in same directory, not root item",
files: []*models.CommitFile{
{
Path: "dir1/a",
},
{
Path: "dir1/b",
},
},
showRootItem: false,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
{
File: &models.CommitFile{Path: "dir1/a"},
path: "dir1/a",
CompressionLevel: 0,
},
{
File: &models.CommitFile{Path: "dir1/b"},
path: "dir1/b",
CompressionLevel: 0,
},
},
},
},
{
name: "paths that can be compressed",
files: []*models.CommitFile{
@@ -735,7 +502,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
Path: "dir2/b",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -762,7 +528,6 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
Path: "a",
},
},
showRootItem: true,
expected: &Node[models.CommitFile]{
path: "",
Children: []*Node[models.CommitFile]{
@@ -781,7 +546,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := BuildFlatTreeFromCommitFiles(s.files, s.showRootItem)
result := BuildFlatTreeFromCommitFiles(s.files)
assert.EqualValues(t, s.expected, result)
})
}
+6 -7
View File
@@ -2,9 +2,9 @@ package filetree
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type ICommitFileTree interface {
@@ -21,7 +21,7 @@ type CommitFileTree struct {
getFiles func() []*models.CommitFile
tree *Node[models.CommitFile]
showTree bool
common *common.Common
log *logrus.Entry
collapsedPaths *CollapsedPaths
}
@@ -41,10 +41,10 @@ func (self *CommitFileTree) ExpandAll() {
var _ ICommitFileTree = &CommitFileTree{}
func NewCommitFileTree(getFiles func() []*models.CommitFile, common *common.Common, showTree bool) *CommitFileTree {
func NewCommitFileTree(getFiles func() []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTree {
return &CommitFileTree{
getFiles: getFiles,
common: common,
log: log,
showTree: showTree,
collapsedPaths: NewCollapsedPaths(),
}
@@ -94,11 +94,10 @@ func (self *CommitFileTree) GetAllFiles() []*models.CommitFile {
}
func (self *CommitFileTree) SetTree() {
showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree
if self.showTree {
self.tree = BuildTreeFromCommitFiles(self.getFiles(), showRootItem)
self.tree = BuildTreeFromCommitFiles(self.getFiles())
} else {
self.tree = BuildFlatTreeFromCommitFiles(self.getFiles(), showRootItem)
self.tree = BuildFlatTreeFromCommitFiles(self.getFiles())
}
}
@@ -5,10 +5,10 @@ import (
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type ICommitFileTreeViewModel interface {
@@ -43,8 +43,8 @@ type CommitFileTreeViewModel struct {
var _ ICommitFileTreeViewModel = &CommitFileTreeViewModel{}
func NewCommitFileTreeViewModel(getFiles func() []*models.CommitFile, common *common.Common, showTree bool) *CommitFileTreeViewModel {
fileTree := NewCommitFileTree(getFiles, common, showTree)
func NewCommitFileTreeViewModel(getFiles func() []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTreeViewModel {
fileTree := NewCommitFileTree(getFiles, log, showTree)
listCursor := traits.NewListCursor(fileTree.Len)
return &CommitFileTreeViewModel{
ICommitFileTree: fileTree,
+6 -7
View File
@@ -4,9 +4,9 @@ import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type FileTreeDisplayFilter int
@@ -54,17 +54,17 @@ type FileTree struct {
getFiles func() []*models.File
tree *Node[models.File]
showTree bool
common *common.Common
log *logrus.Entry
filter FileTreeDisplayFilter
collapsedPaths *CollapsedPaths
}
var _ IFileTree = &FileTree{}
func NewFileTree(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTree {
func NewFileTree(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTree {
return &FileTree{
getFiles: getFiles,
common: common,
log: log,
showTree: showTree,
filter: DisplayAll,
collapsedPaths: NewCollapsedPaths(),
@@ -168,11 +168,10 @@ func (self *FileTree) GetAllFiles() []*models.File {
func (self *FileTree) SetTree() {
filesForDisplay := self.getFilesForDisplay()
showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree
if self.showTree {
self.tree = BuildTreeFromFiles(filesForDisplay, showRootItem)
self.tree = BuildTreeFromFiles(filesForDisplay)
} else {
self.tree = BuildFlatTreeFromFiles(filesForDisplay, showRootItem)
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
}
}
+3 -3
View File
@@ -5,11 +5,11 @@ import (
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type IFileTreeViewModel interface {
@@ -28,8 +28,8 @@ type FileTreeViewModel struct {
var _ IFileTreeViewModel = &FileTreeViewModel{}
func NewFileTreeViewModel(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTreeViewModel {
fileTree := NewFileTree(getFiles, common, showTree)
func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
fileTree := NewFileTree(getFiles, log, showTree)
listCursor := traits.NewListCursor(fileTree.Len)
return &FileTreeViewModel{
IFileTree: fileTree,
-7
View File
@@ -523,13 +523,6 @@ func (gui *Gui) checkForChangedConfigsThatDontAutoReload(oldConfig *config.UserC
// resetState reuses the repo state from our repo state map, if the repo was
// open before; otherwise it creates a new one.
func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
// Un-highlight the current view if there is one. The reason we do this is
// that the repo we are switching to might have a different view focused,
// and would then show an inactive highlight for the previous view.
if oldCurrentView := gui.g.CurrentView(); oldCurrentView != nil {
oldCurrentView.Highlight = false
}
worktreePath := gui.git.RepoPaths.WorktreePath()
if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil {
+11 -14
View File
@@ -523,22 +523,19 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
}
func (gui *Gui) callKeybindingHandler(binding *types.Binding) error {
var disabledReason *types.DisabledReason
if binding.GetDisabledReason != nil {
if disabledReason := binding.GetDisabledReason(); disabledReason != nil {
if disabledReason.AllowFurtherDispatching {
return &types.ErrKeybindingNotHandled{DisabledReason: disabledReason}
}
if disabledReason.ShowErrorInPanel {
return errors.New(disabledReason.Text)
}
if len(disabledReason.Text) > 0 {
gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text)
}
return nil
}
disabledReason = binding.GetDisabledReason()
}
if disabledReason != nil {
if disabledReason.ShowErrorInPanel {
return errors.New(disabledReason.Text)
}
if len(disabledReason.Text) > 0 {
gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text)
}
return nil
}
return binding.Handler()
}
+2 -4
View File
@@ -1,8 +1,6 @@
package gui
import (
"errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
@@ -123,7 +121,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
_, err := setViewFromDimensions(context)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
if err != nil && !gocui.IsUnknownView(err) {
return err
}
}
@@ -136,7 +134,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
for _, context := range gui.transientContexts() {
view, err := gui.g.View(context.GetViewName())
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
if err != nil && !gocui.IsUnknownView(err) {
return err
}
view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName()
-11
View File
@@ -2,7 +2,6 @@ package popup
import (
"context"
"errors"
"strings"
"github.com/jesseduffield/gocui"
@@ -81,16 +80,6 @@ func (self *PopupHandler) WithWaitingStatusSync(message string, f func() error)
}
func (self *PopupHandler) ErrorHandler(err error) error {
var notHandledError *types.ErrKeybindingNotHandled
if errors.As(err, &notHandledError) {
if !notHandledError.DisabledReason.ShowErrorInPanel {
if msg := notHandledError.DisabledReason.Text; len(msg) > 0 {
self.ErrorToast(self.Tr.DisabledMenuItemPrefix + msg)
}
return nil
}
}
// Need to set bold here explicitly; otherwise it gets cancelled by the red colouring.
coloredMessage := style.FgRed.SetBold().Sprint(strings.TrimSpace(err.Error()))
if err := self.onErrorFn(); err != nil {
+4 -6
View File
@@ -25,13 +25,12 @@ func RenderFileTree(
showFileIcons bool,
showNumstat bool,
customIconsConfig *config.CustomIconsConfig,
showRootItem bool,
) []string {
collapsedPaths := tree.CollapsedPaths()
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
fileNode := filetree.NewFileNode(node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig, showRootItem)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig)
})
}
@@ -121,9 +120,8 @@ func getFileLine(
submoduleConfigs []*models.SubmoduleConfig,
node *filetree.Node[models.File],
customIconsConfig *config.CustomIconsConfig,
showRootItem bool,
) string {
name := fileNameAtDepth(node, treeDepth, showRootItem)
name := fileNameAtDepth(node, treeDepth)
output := ""
var nameColor style.TextStyle
@@ -299,7 +297,7 @@ func getColorForChangeStatus(changeStatus string) style.TextStyle {
}
}
func fileNameAtDepth(node *filetree.Node[models.File], depth int, showRootItem bool) string {
func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
splitName := split(node.GetInternalPath())
if depth == 0 && splitName[0] == "." {
if len(splitName) == 1 {
@@ -310,7 +308,7 @@ func fileNameAtDepth(node *filetree.Node[models.File], depth int, showRootItem b
name := join(splitName[depth:])
if node.File != nil && node.File.IsRename() {
splitPrevName := filetree.SplitFileTreePath(node.File.PreviousPath, showRootItem)
splitPrevName := split("./" + node.File.PreviousPath)
prevName := node.File.PreviousPath
// if the file has just been renamed inside the same directory, we can shave off
+5 -65
View File
@@ -7,7 +7,6 @@ import (
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -26,7 +25,6 @@ func TestRenderFileTree(t *testing.T) {
files []*models.File
collapsedPaths []string
showLineChanges bool
showRootItem bool
expected []string
}{
{
@@ -39,8 +37,7 @@ func TestRenderFileTree(t *testing.T) {
files: []*models.File{
{Path: "test", ShortStatus: " M", HasStagedChanges: true},
},
showRootItem: true,
expected: []string{" M test"},
expected: []string{" M test"},
},
{
name: "numstat",
@@ -51,7 +48,6 @@ func TestRenderFileTree(t *testing.T) {
{Path: "test4", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 0, LinesDeleted: 0},
},
showLineChanges: true,
showRootItem: true,
expected: []string{
"▼ /",
" M test +1 -1",
@@ -70,7 +66,6 @@ func TestRenderFileTree(t *testing.T) {
{Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
{Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
showRootItem: true,
expected: toStringSlice(
`
▼ /
@@ -85,30 +80,6 @@ func TestRenderFileTree(t *testing.T) {
),
collapsedPaths: []string{"./dir1"},
},
{
name: "big example without root item",
files: []*models.File{
{Path: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
{Path: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
{Path: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
{Path: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
{Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
{Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
showRootItem: false,
expected: toStringSlice(
`
▶ dir1
▼ dir2
▼ dir2
M file3
M file4
M file5
M file1
`,
),
collapsedPaths: []string{"dir1"},
},
}
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
@@ -116,14 +87,12 @@ M file1
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
common := common.NewDummyCommon()
common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, common, true)
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
viewModel.SetTree()
for _, path := range s.collapsedPaths {
viewModel.ToggleCollapsed(path)
}
result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{}, s.showRootItem)
result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{})
assert.EqualValues(t, s.expected, result)
})
}
@@ -135,7 +104,6 @@ func TestRenderCommitFileTree(t *testing.T) {
root *filetree.FileNode
files []*models.CommitFile
collapsedPaths []string
showRootItem bool
expected []string
}{
{
@@ -148,8 +116,7 @@ func TestRenderCommitFileTree(t *testing.T) {
files: []*models.CommitFile{
{Path: "test", ChangeStatus: "A"},
},
showRootItem: true,
expected: []string{"A test"},
expected: []string{"A test"},
},
{
name: "big example",
@@ -161,7 +128,6 @@ func TestRenderCommitFileTree(t *testing.T) {
{Path: "dir2/file5", ChangeStatus: "M"},
{Path: "file1", ChangeStatus: "M"},
},
showRootItem: true,
expected: toStringSlice(
`
▼ /
@@ -176,30 +142,6 @@ func TestRenderCommitFileTree(t *testing.T) {
),
collapsedPaths: []string{"./dir1"},
},
{
name: "big example without root item",
files: []*models.CommitFile{
{Path: "dir1/file2", ChangeStatus: "M"},
{Path: "dir1/file3", ChangeStatus: "A"},
{Path: "dir2/dir2/file3", ChangeStatus: "D"},
{Path: "dir2/dir2/file4", ChangeStatus: "M"},
{Path: "dir2/file5", ChangeStatus: "M"},
{Path: "file1", ChangeStatus: "M"},
},
showRootItem: false,
expected: toStringSlice(
`
▶ dir1
▼ dir2
▼ dir2
D file3
M file4
M file5
M file1
`,
),
collapsedPaths: []string{"dir1"},
},
}
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
@@ -209,9 +151,7 @@ M file1
t.Run(s.name, func(t *testing.T) {
hashPool := &utils.StringPool{}
common := common.NewDummyCommon()
common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, common, true)
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, utils.NewDummyLog(), true)
viewModel.SetRef(models.NewCommit(hashPool, models.NewCommitOpts{Hash: "1234"}))
viewModel.SetTree()
for _, path := range s.collapsedPaths {
-4
View File
@@ -202,10 +202,6 @@ type DisabledReason struct {
// error panel instead. This is useful if the text is very long, or if it is
// important enough to show it more prominently, or both.
ShowErrorInPanel bool
// If true, the keybinding dispatch mechanism will continue to look for
// other handlers for the keypress.
AllowFurtherDispatching bool
}
type MenuWidget int
-12
View File
@@ -55,15 +55,3 @@ type KeybindingGuards struct {
OutsideFilterMode Guard
NoPopupPanel Guard
}
type ErrKeybindingNotHandled struct {
DisabledReason *DisabledReason
}
func (e ErrKeybindingNotHandled) Error() string {
return e.DisabledReason.Text
}
func (e ErrKeybindingNotHandled) Unwrap() error {
return gocui.ErrKeybindingNotHandled
}
+1 -2
View File
@@ -1,7 +1,6 @@
package gui
import (
"errors"
"fmt"
"github.com/jesseduffield/gocui"
@@ -79,7 +78,7 @@ func (gui *Gui) createAllViews() error {
var err error
for _, mapping := range gui.orderedViewNameMappings() {
*mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
if err != nil && !gocui.IsUnknownView(err) {
return err
}
}
+1 -3
View File
@@ -369,7 +369,6 @@ type TranslationSet struct {
RewordNotSupported string
ChangingThisActionIsNotAllowed string
NotAllowedMidCherryPickOrRevert string
PickIsOnlyAllowedDuringRebase string
DroppingMergeRequiresSingleSelection string
CherryPickCopy string
CherryPickCopyTooltip string
@@ -1460,7 +1459,6 @@ func EnglishTranslationSet() *TranslationSet {
RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported",
ChangingThisActionIsNotAllowed: "Changing this kind of rebase todo entry is not allowed",
NotAllowedMidCherryPickOrRevert: "This action is not allowed while cherry-picking or reverting",
PickIsOnlyAllowedDuringRebase: "This action is only allowed while rebasing",
DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item",
CherryPickCopy: "Copy (cherry-pick)",
CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.",
@@ -1531,7 +1529,7 @@ func EnglishTranslationSet() *TranslationSet {
DiscardFileChangesTitle: "Discard file changes",
DiscardFileChangesPrompt: "Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.",
DisabledForGPG: "Feature not available for users using GPG.\n\nIf you are using a passphrase agent (e.g. gpg-agent) so that you don't have to type your passphrase when signing, you can enable this feature by adding\n\ngit:\n overrideGpg: true\n\nto your lazygit config file.",
CreateRepo: "Not in a git repository. Create a new git repository? (y/N): ",
CreateRepo: "Not in a git repository. Create a new git repository? (y/n): ",
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not yet support bare repos. Open most recent repo? (y/n) ",
InitialBranch: "Branch name? (leave empty for git's default): ",
NoRecentRepositories: "Must open lazygit in a git repository. No valid recent repositories. Exiting.",
+4 -5
View File
@@ -1,7 +1,6 @@
package clients
import (
"errors"
"fmt"
"log"
"os"
@@ -315,7 +314,7 @@ func (self *app) layout(g *gocui.Gui) error {
g.FgColor = gocui.ColorGreen
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
if !gocui.IsUnknownView(err) {
return err
}
@@ -335,7 +334,7 @@ func (self *app) layout(g *gocui.Gui) error {
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
if !gocui.IsUnknownView(err) {
return err
}
descriptionView.Title = "Test description"
@@ -345,7 +344,7 @@ func (self *app) layout(g *gocui.Gui) error {
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
if !gocui.IsUnknownView(err) {
return err
}
keybindingsView.Title = "Keybindings"
@@ -356,7 +355,7 @@ func (self *app) layout(g *gocui.Gui) error {
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
if !gocui.IsUnknownView(err) {
return err
}
@@ -9,9 +9,7 @@ var MoveCommitsToNewBranchKeepStacked = NewIntegrationTest(NewIntegrationTestArg
Description: "Create a new branch from the commits that you accidentally made on the wrong branch; choosing stacked on current branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.BranchPrefix = "myprefix/"
},
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
shell.CloneIntoRemote("origin")
@@ -44,13 +42,12 @@ var MoveCommitsToNewBranchKeepStacked = NewIntegrationTest(NewIntegrationTestArg
t.ExpectPopup().Prompt().
Title(Equals("New branch name (branch is off of 'feature')")).
InitialText(Equals("myprefix/")).
Type("new branch").
Confirm()
t.Views().Branches().
Lines(
Contains("myprefix/new-branch").DoesNotContain("↑").IsSelected(),
Contains("new-branch").DoesNotContain("↑").IsSelected(),
Contains("feature ✓"),
Contains("master ✓"),
)
@@ -1,52 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ResetToDuplicateNamedTag = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Hard reset to a branch when a tag shares the same name",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("current-branch")
shell.EmptyCommit("other-branch-tag commit")
shell.CreateLightweightTag("other-branch", "HEAD")
shell.EmptyCommit("other-branch commit")
shell.NewBranch("other-branch")
shell.Checkout("current-branch")
shell.EmptyCommit("current-branch commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().Lines(
Contains("current-branch commit"),
Contains("other-branch commit"),
Contains("other-branch-tag commit"),
)
t.Views().Branches().
Focus().
Lines(
Contains("current-branch").IsSelected(),
Contains("other-branch"),
).
SelectNextItem().
Press(keys.Commits.ViewResetOptions)
t.ExpectPopup().Menu().
Title(Contains("Reset to other-branch")).
Select(Contains("Hard reset")).
Confirm()
t.Views().Commits().
Lines(
Contains("other-branch commit"),
Contains("other-branch-tag commit"),
)
},
})
@@ -1,57 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ResetToDuplicateNamedUpstream = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Hard reset the current branch to an upstream branch when there is a competing tag name",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
CloneIntoRemote("origin").
NewBranch("foo").
EmptyCommit("commit 1").
PushBranchAndSetUpstream("origin", "foo").
EmptyCommit("commit 2").
CreateLightweightTag("origin/foo", "HEAD")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().Lines(
Contains("commit 2"),
Contains("commit 1"),
)
t.Views().Tags().Focus().Lines(Contains("origin/foo"))
t.Views().Remotes().Focus().
Lines(Contains("origin")).
PressEnter()
t.Views().RemoteBranches().IsFocused().
Lines(Contains("foo")).
Press(keys.Commits.ViewResetOptions)
t.ExpectPopup().Menu().
Title(Contains("Reset to origin/foo")).
Select(Contains("Hard reset")).
Confirm()
t.Views().Commits().Lines(
Contains("commit 1"),
)
t.Views().Tags().Focus().
Lines(Contains("origin/foo")).
Press(keys.Commits.ViewResetOptions)
t.ExpectPopup().Menu().
Title(Contains("Reset to origin/foo")).
Select(Contains("Hard reset")).
Confirm()
t.Views().Commits().Lines(
Contains("commit 2"),
Contains("commit 1"),
)
},
})
@@ -1,36 +0,0 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RenamedFilesNoRootItem = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Regression test for the display of renamed files in the file tree, when the root item is disabled",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.ShowRootItemInFileTree = false
},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir")
shell.CreateDir("dir/nested")
shell.CreateFileAndAdd("file1", "file1 content\n")
shell.CreateFileAndAdd("dir/file2", "file2 content\n")
shell.CreateFileAndAdd("dir/nested/file3", "file3 content\n")
shell.Commit("initial commit")
shell.RunCommand([]string{"git", "mv", "file1", "dir/file1"})
shell.RunCommand([]string{"git", "mv", "dir/file2", "dir/file2-renamed"})
shell.RunCommand([]string{"git", "mv", "dir/nested/file3", "file3"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Equals("▼ dir"),
Equals(" R file1 → file1"),
Equals(" R file2 → file2-renamed"),
Equals("R dir/nested/file3 → file3"),
)
},
})
@@ -1,48 +0,0 @@
package tag
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ResetToDuplicateNamedBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Hard reset to a tag when a branch shares the same name",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("current-branch")
shell.EmptyCommit("other-branch-tag commit")
shell.CreateLightweightTag("other-branch", "HEAD")
shell.EmptyCommit("other-branch commit")
shell.NewBranch("other-branch")
shell.Checkout("current-branch")
shell.EmptyCommit("current-branch commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().Lines(
Contains("current-branch commit"),
Contains("other-branch commit"),
Contains("other-branch-tag commit"),
)
t.Views().Tags().
Focus().
Lines(
Contains("other-branch").IsSelected(),
).
Press(keys.Commits.ViewResetOptions)
t.ExpectPopup().Menu().
Title(Contains("Reset to other-branch")).
Select(Contains("Hard reset")).
Confirm()
t.Views().Commits().Lines(
Contains("other-branch-tag commit"),
)
},
})
-4
View File
@@ -71,8 +71,6 @@ var tests = []*components.IntegrationTest{
branch.RebaseToUpstream,
branch.Rename,
branch.Reset,
branch.ResetToDuplicateNamedTag,
branch.ResetToDuplicateNamedUpstream,
branch.ResetToUpstream,
branch.SelectCommitsOfCurrentBranch,
branch.SetUpstream,
@@ -209,7 +207,6 @@ var tests = []*components.IntegrationTest{
file.RememberCommitMessageAfterFail,
file.RenameSimilarityThresholdChange,
file.RenamedFiles,
file.RenamedFilesNoRootItem,
file.StageChildrenRangeSelect,
file.StageDeletedRangeSelect,
file.StageRangeSelect,
@@ -408,7 +405,6 @@ var tests = []*components.IntegrationTest{
tag.ForceTagAnnotated,
tag.ForceTagLightweight,
tag.Reset,
tag.ResetToDuplicateNamedBranch,
ui.Accordion,
ui.DisableSwitchTabWithPanelJumpKeys,
ui.EmptyMenu,
-5
View File
@@ -569,11 +569,6 @@
"description": "If true, display the files in the file views as a tree. If false, display the files as a flat list.\nThis can be toggled from within Lazygit with the '`' key, but that will not change the default.",
"default": true
},
"showRootItemInFileTree": {
"type": "boolean",
"description": "If true, add a \"/\" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.",
"default": true
},
"showNumstatInFilesView": {
"type": "boolean",
"description": "If true, show the number of lines changed per file in the Files view",
+42 -15
View File
@@ -786,7 +786,7 @@ func (g *Gui) MainLoop() error {
}
func (g *Gui) handleError(err error) error {
if err != nil && !standardErrors.Is(err, ErrQuit) && g.ErrorHandler != nil {
if err != nil && !IsQuit(err) && g.ErrorHandler != nil {
return g.ErrorHandler(err)
}
@@ -1232,9 +1232,24 @@ func (g *Gui) draw(v *View) error {
if g.Cursor {
if curview := g.currentView; curview != nil {
vMaxX, vMaxY := curview.InnerSize()
if curview.cx >= 0 && curview.cx < vMaxX && curview.cy >= 0 && curview.cy < vMaxY {
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
vMaxX, vMaxY := curview.Size()
if curview.cx < 0 {
curview.cx = 0
} else if curview.cx >= vMaxX {
curview.cx = vMaxX - 1
}
if curview.cy < 0 {
curview.cy = 0
} else if curview.cy >= vMaxY {
curview.cy = vMaxY - 1
}
gMaxX, gMaxY := g.Size()
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
// This test probably doesn't need to be here.
// tcell is hiding cursor by setting coordinates outside of screen.
// Keeping it here for now, as I'm not 100% sure :)
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
Screen.ShowCursor(cx, cy)
} else {
Screen.HideCursor()
@@ -1498,8 +1513,6 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
}
}
var err error
for _, kb := range g.keybindings {
if kb.handler == nil {
continue
@@ -1508,13 +1521,13 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
continue
}
if g.matchView(v, kb) {
err = g.execKeybinding(v, kb)
if !errors.Is(err, ErrKeybindingNotHandled) {
err := g.execKeybinding(v, kb)
if IsKeybindingNotHandled(err) {
matchingParentViewKb = nil
break
} else {
return err
}
matchingParentViewKb = nil
break
}
if v != nil && g.matchView(v.ParentView, kb) {
matchingParentViewKb = kb
@@ -1524,8 +1537,8 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
}
}
if matchingParentViewKb != nil {
err = g.execKeybinding(v.ParentView, matchingParentViewKb)
if !errors.Is(err, ErrKeybindingNotHandled) {
err := g.execKeybinding(v.ParentView, matchingParentViewKb)
if !IsKeybindingNotHandled(err) {
return err
}
}
@@ -1538,9 +1551,9 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
}
if globalKb != nil {
err = g.execKeybinding(v, globalKb)
return g.execKeybinding(v, globalKb)
}
return err
return nil
}
// execKeybinding executes a given keybinding
@@ -1604,6 +1617,20 @@ func (g *Gui) isBlacklisted(k Key) bool {
return false
}
// IsUnknownView reports whether the contents of an error is "unknown view".
func IsUnknownView(err error) bool {
return err != nil && err.Error() == ErrUnknownView.Error()
}
// IsQuit reports whether the contents of an error is "quit".
func IsQuit(err error) bool {
return err != nil && err.Error() == ErrQuit.Error()
}
func IsKeybindingNotHandled(err error) bool {
return err != nil && err.Error() == ErrKeybindingNotHandled.Error()
}
func (g *Gui) Suspend() error {
g.suspendedMutex.Lock()
defer g.suspendedMutex.Unlock()
+16 -7
View File
@@ -562,19 +562,29 @@ func max(a, b int) int {
}
// SetCursor sets the cursor position of the view at the given point,
// relative to the view. It is allowed to set the position to a point outside
// the visible portion of the view, or even outside the content of the view.
// Clients are responsible for clamping to valid positions.
// relative to the view. It checks if the position is valid.
func (v *View) SetCursor(x, y int) {
maxX, maxY := v.InnerSize()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return
}
v.cx = x
v.cy = y
}
func (v *View) SetCursorX(x int) {
maxX := v.InnerWidth()
if x < 0 || x >= maxX {
return
}
v.cx = x
}
func (v *View) SetCursorY(y int) {
maxY := v.InnerHeight()
if y < 0 || y >= maxY {
return
}
v.cy = y
}
@@ -1675,11 +1685,10 @@ func (v *View) RenderTextArea() {
func updatedCursorAndOrigin(prevOrigin int, size int, cursor int) (int, int) {
var newViewCursor int
newOrigin := prevOrigin
usableSize := size - 1
if cursor > prevOrigin+usableSize {
newOrigin = cursor - usableSize
newViewCursor = usableSize
if cursor > prevOrigin+size {
newOrigin = cursor - size
newViewCursor = size
} else if cursor < prevOrigin {
newOrigin = cursor
newViewCursor = 0
+1 -1
View File
@@ -227,7 +227,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
github.com/jesseduffield/go-git/v5/utils/sync
github.com/jesseduffield/go-git/v5/utils/trace
# github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412
# github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a