1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-02 22:25:47 +02:00
lazygit/pkg/gui/controllers/files_controller.go
Jesse Duffield f3eb180f75 Standardise display of range selection across views
We're not fully standardising here: different contexts can store their range state however
they like. What we are standardising on is that now the view is always responsible for
highlighting the selected lines, meaning the context/controller needs to tell the view
where the range start is.

Two convenient benefits from this change:
1) we no longer need bespoke code in integration tests for asserting on selected lines because
we can just ask the view
2) line selection in staging/patch-building/merge-conflicts views now look the same as in
list views i.e. the highlight applies to the whole line (including trailing space)

I also noticed a bug with merge conflicts not rendering the selection on focus though I suspect
it wasn't a bug with any real consequences when the view wasn't displaying the selection.

I'm going to scrap the selectedRangeBgColor config and just let it use the single line
background color. Hopefully nobody cares, but there's really no need for an extra config.
2024-01-19 10:47:21 +11:00

958 lines
27 KiB
Go

package controllers
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FilesController struct {
baseController // nolint: unused
c *ControllerCommon
}
var _ types.IController = &FilesController{}
func NewFilesController(
common *ControllerCommon,
) *FilesController {
return &FilesController{
c: common,
}
}
func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelectedFileNode(self.press),
Description: self.c.Tr.ToggleStaged,
},
{
Key: opts.GetKey(opts.Config.Files.OpenStatusFilter),
Handler: self.handleStatusFilterPressed,
Description: self.c.Tr.FileFilter,
},
{
Key: opts.GetKey(opts.Config.Files.CopyFileInfoToClipboard),
Handler: self.openCopyMenu,
Description: self.c.Tr.CopyToClipboardMenu,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.CommitChanges),
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
Description: self.c.Tr.CommitChanges,
},
{
Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook),
Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress,
Description: self.c.Tr.CommitChangesWithoutHook,
},
{
Key: opts.GetKey(opts.Config.Files.AmendLastCommit),
Handler: self.handleAmendCommitPress,
Description: self.c.Tr.AmendLastCommit,
},
{
Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor),
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
Description: self.c.Tr.CommitChangesWithEditor,
},
{
Key: opts.GetKey(opts.Config.Files.FindBaseCommitForFixup),
Handler: self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress,
Description: self.c.Tr.FindBaseCommitForFixup,
Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip,
},
{
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelectedFileNode(self.edit),
Description: self.c.Tr.EditFile,
},
{
Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.Open,
Description: self.c.Tr.OpenFile,
},
{
Key: opts.GetKey(opts.Config.Files.IgnoreFile),
Handler: self.checkSelectedFileNode(self.ignoreOrExcludeMenu),
Description: self.c.Tr.Actions.IgnoreExcludeFile,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.RefreshFiles),
Handler: self.refresh,
Description: self.c.Tr.RefreshFiles,
},
{
Key: opts.GetKey(opts.Config.Files.StashAllChanges),
Handler: self.stash,
Description: self.c.Tr.StashAllChanges,
},
{
Key: opts.GetKey(opts.Config.Files.ViewStashOptions),
Handler: self.createStashMenu,
Description: self.c.Tr.ViewStashOptions,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.ToggleStagedAll),
Handler: self.toggleStagedAll,
Description: self.c.Tr.ToggleStagedAll,
},
{
Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.enter,
Description: self.c.Tr.FileEnter,
},
{
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.createResetToUpstreamMenu,
Description: self.c.Tr.ViewResetToUpstreamOptions,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.ViewResetOptions),
Handler: self.createResetMenu,
Description: self.c.Tr.ViewResetOptions,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Files.ToggleTreeView),
Handler: self.toggleTreeView,
Description: self.c.Tr.ToggleTreeView,
},
{
Key: opts.GetKey(opts.Config.Universal.OpenDiffTool),
Handler: self.checkSelectedFileNode(self.openDiffTool),
Description: self.c.Tr.OpenDiffTool,
},
{
Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.OpenMergeTool,
},
{
Key: opts.GetKey(opts.Config.Files.Fetch),
Handler: self.fetch,
Description: self.c.Tr.Fetch,
},
}
}
func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: "main",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuilding",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "mergeConflicts",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "secondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuildingSecondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
},
}
}
func (self *FilesController) GetOnRenderToMain() func() error {
return func() error {
return self.c.Helpers().Diff.WithDiffModeCheck(func() error {
node := self.context().GetSelected()
if node == nil {
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.DiffTitle,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles),
},
})
}
if node.File != nil && node.File.HasInlineMergeConflicts {
hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.GetPath())
if err != nil {
return err
}
if hasConflicts {
return self.c.Helpers().MergeConflicts.Render()
}
}
self.c.Helpers().MergeConflicts.ResetMergeState()
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().Staging
}
split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged)
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
}
refreshOpts := types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Title: title,
},
}
if split {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true)
title := self.c.Tr.StagedChanges
if mainShowsStaged {
title = self.c.Tr.UnstagedChanges
}
refreshOpts.Secondary = &types.ViewUpdateOpts{
Title: title,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
}
}
return self.c.RenderToMainViews(refreshOpts)
})
}
}
func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press)
}
// if we are dealing with a status for which there is no key in this map,
// then we won't optimistically render: we'll just let `git status` tell
// us what the new status is.
// There are no doubt more entries that could be added to these two maps.
var stageStatusMap = map[string]string{
"??": "A ",
" M": "M ",
"MM": "M ",
" D": "D ",
" A": "A ",
"AM": "A ",
"MD": "D ",
}
var unstageStatusMap = map[string]string{
"A ": "??",
"M ": " M",
"D ": " D",
}
func (self *FilesController) optimisticStage(file *models.File) bool {
newShortStatus, ok := stageStatusMap[file.ShortStatus]
if !ok {
return false
}
models.SetStatusFields(file, newShortStatus)
return true
}
func (self *FilesController) optimisticUnstage(file *models.File) bool {
newShortStatus, ok := unstageStatusMap[file.ShortStatus]
if !ok {
return false
}
models.SetStatusFields(file, newShortStatus)
return true
}
// Running a git add command followed by a git status command can take some time (e.g. 200ms).
// Given how often users stage/unstage files in Lazygit, we're adding some
// optimistic rendering to make things feel faster. When we go to stage
// a file, we'll first update that file's status in-memory, then re-render
// the files panel. Then we'll immediately do a proper git status call
// so that if the optimistic rendering got something wrong, it's quickly
// corrected.
func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisticChangeFn func(*models.File) bool) error {
rerender := false
err := node.ForEachFile(func(f *models.File) error {
// can't act on the file itself: we need to update the original model file
for _, modelFile := range self.c.Model().Files {
if modelFile.Name == f.Name {
if optimisticChangeFn(modelFile) {
rerender = true
}
break
}
}
return nil
})
if err != nil {
return err
}
if rerender {
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
return err
}
}
return nil
}
func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
// Obtaining this lock because optimistic rendering requires us to mutate
// the files in our model.
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
if node.IsFile() {
file := node.File
if file.HasUnstagedChanges {
self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
if err := self.c.Git().WorkingTree.StageFile(file.Name); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
}
if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err)
}
}
} else {
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
if err := self.c.Git().WorkingTree.StageFile(node.Path); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
}
// pretty sure it doesn't matter that we're always passing true here
if err := self.c.Git().WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return self.c.Error(err)
}
}
}
return nil
}
func (self *FilesController) press(node *filetree.FileNode) error {
if node.IsFile() && node.File.HasInlineMergeConflicts {
return self.switchToMerge()
}
if err := self.pressWithLock(node); err != nil {
return err
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}); err != nil {
return err
}
return self.context().HandleFocus(types.OnFocusOpts{})
}
func (self *FilesController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error {
return func() error {
node := self.context().GetSelected()
if node == nil {
return nil
}
return callback(node)
}
}
func (self *FilesController) Context() types.Context {
return self.context()
}
func (self *FilesController) context() *context.WorkingTreeContext {
return self.c.Contexts().Files
}
func (self *FilesController) getSelectedFile() *models.File {
node := self.context().GetSelected()
if node == nil {
return nil
}
return node.File
}
func (self *FilesController) enter() error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1})
}
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
node := self.context().GetSelected()
if node == nil {
return nil
}
if node.File == nil {
return self.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := self.c.Model().Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return self.c.Helpers().Repos.EnterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return self.switchToMerge()
}
if file.HasMergeConflicts {
return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements)
}
return self.c.PushContext(self.c.Contexts().Staging, opts)
}
func (self *FilesController) toggleStagedAll() error {
if err := self.toggleStagedAllWithLock(); err != nil {
return err
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC}); err != nil {
return err
}
return self.context().HandleFocus(types.OnFocusOpts{})
}
func (self *FilesController) toggleStagedAllWithLock() error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
root := self.context().FileTreeViewModel.GetRoot()
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if root.GetHasInlineMergeConflicts() {
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
}
if root.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.optimisticChange(root, self.optimisticStage); err != nil {
return err
}
if err := self.c.Git().WorkingTree.StageAll(); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)
if err := self.optimisticChange(root, self.optimisticUnstage); err != nil {
return err
}
if err := self.c.Git().WorkingTree.UnstageAll(); err != nil {
return self.c.Error(err)
}
}
return nil
}
func (self *FilesController) unstageFiles(node *filetree.FileNode) error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}
func (self *FilesController) ignoreOrExcludeTracked(node *filetree.FileNode, trAction string, f func(string) error) error {
self.c.LogAction(trAction)
// not 100% sure if this is necessary but I'll assume it is
if err := self.unstageFiles(node); err != nil {
return err
}
if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
if err := f(node.GetPath()); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) ignoreOrExcludeUntracked(node *filetree.FileNode, trAction string, f func(string) error) error {
self.c.LogAction(trAction)
if err := f(node.GetPath()); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) ignoreOrExcludeFile(node *filetree.FileNode, trText string, trPrompt string, trAction string, f func(string) error) error {
if node.GetIsTracked() {
return self.c.Confirm(types.ConfirmOpts{
Title: trText,
Prompt: trPrompt,
HandleConfirm: func() error {
return self.ignoreOrExcludeTracked(node, trAction, f)
},
})
}
return self.ignoreOrExcludeUntracked(node, trAction, f)
}
func (self *FilesController) ignore(node *filetree.FileNode) error {
if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr)
}
err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.IgnoreExcludeFile, self.c.Git().WorkingTree.Ignore)
if err != nil {
return err
}
return nil
}
func (self *FilesController) exclude(node *filetree.FileNode) error {
if node.GetPath() == ".git/info/exclude" {
return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeFileErr)
}
if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeGitIgnoreErr)
}
err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude)
if err != nil {
return err
}
return nil
}
func (self *FilesController) ignoreOrExcludeMenu(node *filetree.FileNode) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.IgnoreExcludeFile,
Items: []*types.MenuItem{
{
LabelColumns: []string{self.c.Tr.IgnoreFile},
OnPress: func() error {
if err := self.ignore(node); err != nil {
return self.c.Error(err)
}
return nil
},
Key: 'i',
},
{
LabelColumns: []string{self.c.Tr.ExcludeFile},
OnPress: func() error {
if err := self.exclude(node); err != nil {
return self.c.Error(err)
}
return nil
},
Key: 'e',
},
},
})
}
func (self *FilesController) refresh() error {
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) handleAmendCommitPress() error {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
return self.c.Helpers().WorkingTree.WithEnsureCommitableFiles(func() error {
if len(self.c.Model().Commits) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
}
return self.c.Helpers().AmendHelper.AmendHead()
})
},
})
}
func (self *FilesController) handleStatusFilterPressed() error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FilteringMenuTitle,
Items: []*types.MenuItem{
{
Label: self.c.Tr.FilterStagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayStaged)
},
},
{
Label: self.c.Tr.FilterUnstagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayUnstaged)
},
},
{
Label: self.c.Tr.ResetFilter,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayAll)
},
},
},
})
}
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
self.context().FileTreeViewModel.SetStatusFilter(filter)
return self.c.PostRefreshUpdate(self.context())
}
func (self *FilesController) edit(node *filetree.FileNode) error {
if node.File == nil {
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
}
return self.c.Helpers().Files.EditFile(node.GetPath())
}
func (self *FilesController) Open() error {
node := self.context().GetSelected()
if node == nil {
return nil
}
return self.c.Helpers().Files.OpenFile(node.GetPath())
}
func (self *FilesController) openDiffTool(node *filetree.FileNode) error {
fromCommit := ""
reverse := false
if self.c.Modes().Diffing.Active() {
fromCommit = self.c.Modes().Diffing.Ref
reverse = self.c.Modes().Diffing.Reverse
}
return self.c.RunSubprocessAndRefresh(
self.c.Git().Diff.OpenDiffToolCmdObj(
git_commands.DiffToolCmdOptions{
Filepath: node.Path,
FromCommit: fromCommit,
ToCommit: "",
Reverse: reverse,
IsDirectory: !node.IsFile(),
Staged: !node.GetHasUnstagedChanges(),
}),
)
}
func (self *FilesController) switchToMerge() error {
file := self.getSelectedFile()
if file == nil {
return nil
}
return self.c.Helpers().MergeConflicts.SwitchToMerge(file.Name)
}
func (self *FilesController) createStashMenu() error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.StashOptions,
Items: []*types.MenuItem{
{
Label: self.c.Tr.StashAllChanges,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
},
Key: 'a',
},
{
Label: self.c.Tr.StashAllChangesKeepIndex,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
// if there are no staged files it behaves the same as Stash.Save
return self.handleStashSave(self.c.Git().Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex)
},
Key: 'i',
},
{
Label: self.c.Tr.StashIncludeUntrackedChanges,
OnPress: func() error {
return self.handleStashSave(self.c.Git().Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges)
},
Key: 'U',
},
{
Label: self.c.Tr.StashStagedChanges,
OnPress: func() error {
// there must be something in staging otherwise the current implementation mucks the stash up
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash)
}
return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges)
},
Key: 's',
},
{
Label: self.c.Tr.StashUnstagedChanges,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoFilesToStash)
}
if self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges)
}
// ordinary stash
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashUnstagedChanges)
},
Key: 'u',
},
},
})
}
func (self *FilesController) openCopyMenu() error {
node := self.context().GetSelected()
copyNameItem := &types.MenuItem{
Label: self.c.Tr.CopyFileName,
OnPress: func() error {
if err := self.c.OS().CopyToClipboard(node.Name()); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.FileNameCopiedToast)
return nil
},
Key: 'n',
}
copyPathItem := &types.MenuItem{
Label: self.c.Tr.CopyFilePath,
OnPress: func() error {
if err := self.c.OS().CopyToClipboard(node.Path); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.FilePathCopiedToast)
return nil
},
Key: 'p',
}
copyFileDiffItem := &types.MenuItem{
Label: self.c.Tr.CopySelectedDiff,
Tooltip: self.c.Tr.CopyFileDiffTooltip,
OnPress: func() error {
path := self.context().GetSelectedPath()
hasStaged := self.hasPathStagedChanges(node)
diff, err := self.c.Git().Diff.GetPathDiff(path, hasStaged)
if err != nil {
return self.c.Error(err)
}
if err := self.c.OS().CopyToClipboard(diff); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.FileDiffCopiedToast)
return nil
},
Key: 's',
}
copyAllDiff := &types.MenuItem{
Label: self.c.Tr.CopyAllFilesDiff,
Tooltip: self.c.Tr.CopyFileDiffTooltip,
OnPress: func() error {
hasStaged := self.c.Helpers().WorkingTree.AnyStagedFiles()
diff, err := self.c.Git().Diff.GetAllDiff(hasStaged)
if err != nil {
return self.c.Error(err)
}
if err := self.c.OS().CopyToClipboard(diff); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast)
return nil
},
Key: 'a',
}
if node == nil {
copyNameItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
copyPathItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
}
if node != nil && !node.GetHasStagedOrTrackedChanges() {
copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
}
if !self.anyStagedOrTrackedFile() {
copyAllDiff.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.CopyToClipboardMenu,
Items: []*types.MenuItem{
copyNameItem,
copyPathItem,
copyFileDiffItem,
copyAllDiff,
},
})
}
func (self *FilesController) anyStagedOrTrackedFile() bool {
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
return self.c.Helpers().WorkingTree.AnyTrackedFiles()
}
return true
}
func (self *FilesController) hasPathStagedChanges(node *filetree.FileNode) bool {
return node.SomeFile(func(t *models.File) bool {
return t.HasStagedChanges
})
}
func (self *FilesController) stash() error {
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
}
func (self *FilesController) createResetToUpstreamMenu() error {
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}")
}
func (self *FilesController) handleToggleDirCollapsed() error {
node := self.context().GetSelected()
if node == nil {
return nil
}
self.context().FileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
self.c.Log.Error(err)
}
return nil
}
func (self *FilesController) toggleTreeView() error {
self.context().FileTreeViewModel.ToggleShowTree()
return self.c.PostRefreshUpdate(self.context())
}
func (self *FilesController) handleStashSave(stashFunc func(message string) error, action string) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.StashChanges,
HandleConfirm: func(stashComment string) error {
self.c.LogAction(action)
if err := stashFunc(stashComment); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
},
})
}
func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
}
func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "secondary", ClickedViewLineIdx: opts.Y})
}
func (self *FilesController) fetch() error {
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
if err := self.fetchAux(task); err != nil {
_ = self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (self *FilesController) fetchAux(task gocui.Task) (err error) {
self.c.LogAction("Fetch")
err = self.c.Git().Sync.Fetch(task)
if err != nil && strings.Contains(err.Error(), "exit status 128") {
_ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong)
}
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}