diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/gui/context/list_context_trait.go | 9 | ||||
-rw-r--r-- | pkg/gui/gui.go | 29 | ||||
-rw-r--r-- | pkg/gui/gui_common.go | 9 | ||||
-rw-r--r-- | pkg/gui/layout.go | 18 | ||||
-rw-r--r-- | pkg/gui/types/common.go | 4 | ||||
-rw-r--r-- | pkg/integration/clients/tui.go | 5 | ||||
-rw-r--r-- | pkg/integration/components/test.go | 26 | ||||
-rw-r--r-- | pkg/integration/components/view_driver.go | 24 | ||||
-rw-r--r-- | pkg/integration/tests/test_list.go | 1 | ||||
-rw-r--r-- | pkg/integration/tests/ui/accordion.go | 61 | ||||
-rw-r--r-- | pkg/integration/types/types.go | 3 |
11 files changed, 183 insertions, 6 deletions
diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index e993719d5..de88d3d3b 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -31,7 +31,14 @@ func (self *ListContextTrait) GetList() types.IList { } func (self *ListContextTrait) FocusLine() { - self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx()) + // Doing this at the end of the layout function because we need the view to be + // resized before we focus the line, otherwise if we're in accordion mode + // the view could be squashed and won't how to adjust the cursor/origin + self.c.AfterLayout(func() error { + self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx()) + return nil + }) + self.setFooter() if self.refreshViewportOnChange { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1057a85bf..25ec769a5 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -132,6 +132,8 @@ type Gui struct { helpers *helpers.Helpers integrationTest integrationTypes.IntegrationTest + + afterLayoutFuncs chan func() error } type StateAccessor struct { @@ -458,7 +460,8 @@ func NewGui( PopupMutex: &deadlock.Mutex{}, PtyMutex: &deadlock.Mutex{}, }, - InitialDir: initialDir, + InitialDir: initialDir, + afterLayoutFuncs: make(chan func() error, 1000), } gui.WatchFilesForChanges() @@ -519,9 +522,29 @@ var RuneReplacements = map[rune]string{ } func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) { - playRecording := test != nil && os.Getenv(components.SANDBOX_ENV_VAR) != "true" + runInSandbox := os.Getenv(components.SANDBOX_ENV_VAR) == "true" + playRecording := test != nil && !runInSandbox + + width, height := 0, 0 + if test != nil { + if test.RequiresHeadless() { + if runInSandbox { + panic("Test requires headless, can't run in sandbox") + } + headless = true + } + width, height = test.HeadlessDimensions() + } - g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playRecording, headless, RuneReplacements) + g, err := gocui.NewGui(gocui.NewGuiOpts{ + OutputMode: gocui.OutputTrue, + SupportOverlaps: OverlappingEdges, + PlayRecording: playRecording, + Headless: headless, + RuneReplacements: RuneReplacements, + Width: width, + Height: height, + }) if err != nil { return nil, err } diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index c0d7bd460..ee0e51b26 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -168,3 +168,12 @@ func (self *guiCommon) IsAnyModeActive() bool { func (self *guiCommon) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []*gocui.ViewMouseBinding) { return self.gui.GetInitialKeybindingsWithCustomCommands() } + +func (self *guiCommon) AfterLayout(f func() error) { + select { + case self.gui.afterLayoutFuncs <- f: + default: + // hopefully this never happens + self.gui.c.Log.Error("afterLayoutFuncs channel is full, skipping function") + } +} diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 14f79cb5a..ec192527b 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -149,7 +149,23 @@ func (gui *Gui) layout(g *gocui.Gui) error { // if you run `lazygit --logs` // this will let you see these branches as prettified json // gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4])) - return gui.helpers.Confirmation.ResizeCurrentPopupPanel() + if err := gui.helpers.Confirmation.ResizeCurrentPopupPanel(); err != nil { + return err + } + +outer: + for { + select { + case f := <-gui.afterLayoutFuncs: + if err := f(); err != nil { + return err + } + default: + break outer + } + } + + return nil } func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 4e5ef627f..02dee3b15 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -80,6 +80,10 @@ type IGuiCommon interface { // Runs a function in a goroutine. Use this whenever you want to run a goroutine and keep track of the fact // that lazygit is still busy. See docs/dev/Busy.md OnWorker(f func(gocui.Task)) + // Function to call at the end of our 'layout' function which renders views + // For example, you may want a view's line to be focused only after that view is + // resized, if in accordion mode. + AfterLayout(f func() error) // returns the gocui Gui struct. There is a good chance you don't actually want to use // this struct and instead want to use another method above diff --git a/pkg/integration/clients/tui.go b/pkg/integration/clients/tui.go index b931e8954..7d82a6630 100644 --- a/pkg/integration/clients/tui.go +++ b/pkg/integration/clients/tui.go @@ -28,7 +28,10 @@ func RunTUI() { app := newApp(testDir) app.loadTests() - g, err := gocui.NewGui(gocui.OutputTrue, false, false, false, gui.RuneReplacements) + g, err := gocui.NewGui(gocui.NewGuiOpts{ + OutputMode: gocui.OutputTrue, + RuneReplacements: gui.RuneReplacements, + }) if err != nil { log.Panicln(err) } diff --git a/pkg/integration/components/test.go b/pkg/integration/components/test.go index 464779e38..d17e3f7c5 100644 --- a/pkg/integration/components/test.go +++ b/pkg/integration/components/test.go @@ -19,6 +19,11 @@ import ( // to get the test's name via it's file's path. const unitTestDescription = "test test" +const ( + defaultWidth = 100 + defaultHeight = 100 +) + type IntegrationTest struct { name string description string @@ -32,6 +37,8 @@ type IntegrationTest struct { keys config.KeybindingConfig, ) gitVersion GitVersionRestriction + width int + height int } var _ integrationTypes.IntegrationTest = &IntegrationTest{} @@ -52,6 +59,11 @@ type NewIntegrationTestArgs struct { Skip bool // to run a test only on certain git versions GitVersion GitVersionRestriction + // width and height when running in headless mode, for testing + // the UI in different sizes. + // If these are set, the test must be run in headless mode + Width int + Height int } type GitVersionRestriction struct { @@ -120,6 +132,8 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest { setupConfig: args.SetupConfig, run: args.Run, gitVersion: args.GitVersion, + width: args.Width, + height: args.Height, } } @@ -172,6 +186,18 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) { } } +func (self *IntegrationTest) HeadlessDimensions() (int, int) { + if self.width == 0 && self.height == 0 { + return defaultWidth, defaultHeight + } + + return self.width, self.height +} + +func (self *IntegrationTest) RequiresHeadless() bool { + return self.width != 0 && self.height != 0 +} + func testNameFromCurrentFilePath() string { path := utils.FilePath(3) return TestNameFromFilePath(path) diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index 2c4a23572..d1d1571c7 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -82,6 +82,20 @@ func (self *ViewDriver) TopLines(matchers ...*TextMatcher) *ViewDriver { return self.assertLines(0, matchers...) } +// Asserts on the visible lines of the view. +// Note, this assumes that the view's viewport is filled with lines +func (self *ViewDriver) VisibleLines(matchers ...*TextMatcher) *ViewDriver { + self.validateMatchersPassed(matchers) + self.validateVisibleLineCount(matchers) + + // Get the origin of the view and offset that. + // Note that we don't do any retrying here so if we want to bring back retry logic + // we'll need to update this. + originY := self.getView().OriginY() + + return self.assertLines(originY, matchers...) +} + // asserts that somewhere in the view there are consequetive lines matching the given matchers. func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver { self.validateMatchersPassed(matchers) @@ -212,6 +226,16 @@ func (self *ViewDriver) validateEnoughLines(matchers []*TextMatcher) { }) } +// assumes the view's viewport is filled with lines +func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) { + view := self.getView() + + self.t.assertWithRetries(func() (bool, string) { + count := view.InnerHeight() + 1 + return count == len(matchers), fmt.Sprintf("unexpected number of visible lines in view '%s'. Expected exactly %d, got %d", view.Name(), len(matchers), count) + }) +} + func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver { view := self.getView() diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index af0ac1c5b..caaf2039e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -209,6 +209,7 @@ var tests = []*components.IntegrationTest{ tag.CrudAnnotated, tag.CrudLightweight, tag.Reset, + ui.Accordion, ui.DoublePopup, ui.SwitchTabFromMenu, undo.UndoCheckoutAndDrop, diff --git a/pkg/integration/tests/ui/accordion.go b/pkg/integration/tests/ui/accordion.go new file mode 100644 index 000000000..abfe27dbb --- /dev/null +++ b/pkg/integration/tests/ui/accordion.go @@ -0,0 +1,61 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +// When in acccordion mode, Lazygit looks like this: +// +// ╶─Status─────────────────────────╴┌─Patch──────────────────────────────────────────────────────────┐ +// ╶─Files - Submodules──────0 of 0─╴│commit 6e56dd04b70e548976f7f2928c4d9c359574e2bc ▲ +// ╶─Local branches - Remotes1 of 1─╴│Author: CI <CI@example.com> █ +// ┌─Commits - Reflog───────────────┐│Date: Wed Jul 19 22:00:03 2023 +1000 │ +// │7fe02805 CI commit 12 ▲│ ▼ +// │6e56dd04 CI commit 11 █└────────────────────────────────────────────────────────────────┘ +// │a35c687d CI commit 10 ▼┌─Command log────────────────────────────────────────────────────┐ +// └───────────────────────10 of 20─┘│Random tip: To filter commits by path, press '<c-s>' │ +// ╶─Stash───────────────────0 of 0─╴└────────────────────────────────────────────────────────────────┘ +// <pgup>/<pgdown>: Scroll, <esc>: Cancel, q: Quit, ?: Keybindings, 1-Donate Ask Question unversioned + +var Accordion = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Verify accordion mode kicks in when the screen height is too small", + ExtraCmdArgs: []string{}, + Width: 100, + Height: 10, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(20) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + VisibleLines( + Contains("commit 20").IsSelected(), + Contains("commit 19"), + Contains("commit 18"), + ). + // go past commit 11, then come back, so that it ends up in the centre of the viewport + NavigateToLine(Contains("commit 11")). + NavigateToLine(Contains("commit 10")). + NavigateToLine(Contains("commit 11")). + VisibleLines( + Contains("commit 12"), + Contains("commit 11").IsSelected(), + Contains("commit 10"), + ) + + t.Views().Files(). + Focus() + + // ensure we retain the same viewport upon re-focus + t.Views().Commits(). + Focus(). + VisibleLines( + Contains("commit 12"), + Contains("commit 11").IsSelected(), + Contains("commit 10"), + ) + }, +}) diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index a26ac67af..266304bbf 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -13,6 +13,9 @@ import ( type IntegrationTest interface { Run(GuiDriver) SetupConfig(config *config.AppConfig) + RequiresHeadless() bool + // width and height when running headless + HeadlessDimensions() (int, int) } // this is the interface through which our integration tests interact with the lazygit gui |