mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-15 10:06:26 -04:00
Modifiers were moved into Key in 22169e22f, but the separate Modifier field
on types.Binding and gocui.keybinding was left behind. The keypress matcher
already compares modifiers via Key.Equals, so the old field is never read on
the dispatch path; it just got passed through SetKeybinding and stored.
Drop it from gocui.keybinding, types.Binding, and the SetKeybinding /
DeleteKeybinding signatures, and remove every now-redundant Modifier:
gocui.ModNone struct field. Mouse bindings keep their own Modifier (on
ViewMouseBinding) since that path still consults it.
409 lines
9.8 KiB
Go
409 lines
9.8 KiB
Go
package clients
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazycore/pkg/utils"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
|
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
|
|
|
|
var SLOW_INPUT_DELAY = 600
|
|
|
|
func RunTUI(raceDetector bool) {
|
|
rootDir := utils.GetLazyRootDirectory()
|
|
testDir := filepath.Join(rootDir, "test", "integration")
|
|
|
|
app := newApp(testDir)
|
|
app.loadTests()
|
|
|
|
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
|
OutputMode: gocui.OutputTrue,
|
|
RuneReplacements: gui.RuneReplacements,
|
|
})
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
g.Cursor = false
|
|
|
|
app.g = g
|
|
|
|
g.SetManagerFunc(app.layout)
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyArrowUp), func(*gocui.Gui, *gocui.View) error {
|
|
if app.itemIdx > 0 {
|
|
app.itemIdx--
|
|
}
|
|
listView, err := g.View("list")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
listView.FocusPoint(0, app.itemIdx, true)
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyArrowDown), func(*gocui.Gui, *gocui.View) error {
|
|
if app.itemIdx < len(app.filteredTests)-1 {
|
|
app.itemIdx++
|
|
}
|
|
|
|
listView, err := g.View("list")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
listView.FocusPoint(0, app.itemIdx, true)
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyStrMod("c", gocui.ModCtrl), quit); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('q'), quit); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('s'), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, true, false, raceDetector, 0)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyEnter), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, false, raceDetector, 0)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('t'), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, false, raceDetector, SLOW_INPUT_DELAY)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('d'), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, true, raceDetector, 0)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('o'), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command("sh", "-c", fmt.Sprintf("code -r pkg/integration/tests/%s.go", currentTest.Name()))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('O'), func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command("sh", "-c", fmt.Sprintf("code test/_results/%s", currentTest.Name()))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", gocui.NewKeyRune('/'), func(*gocui.Gui, *gocui.View) error {
|
|
app.filtering = true
|
|
if _, err := g.SetCurrentView("editor"); err != nil {
|
|
return err
|
|
}
|
|
editorView, err := g.View("editor")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
editorView.Clear()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
// not using the editor yet, but will use it to help filter the list
|
|
if err := g.SetKeybinding("editor", gocui.NewKeyName(gocui.KeyEsc), func(*gocui.Gui, *gocui.View) error {
|
|
app.filtering = false
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
|
|
app.filteredTests = app.allTests
|
|
app.renderTests()
|
|
app.editorView.TextArea.Clear()
|
|
app.editorView.Clear()
|
|
app.editorView.Reset()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("editor", gocui.NewKeyName(gocui.KeyEnter), func(*gocui.Gui, *gocui.View) error {
|
|
app.filtering = false
|
|
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
|
|
app.renderTests()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
err = g.MainLoop()
|
|
g.Close()
|
|
if errors.Is(err, gocui.ErrQuit) {
|
|
return
|
|
}
|
|
log.Panicln(err)
|
|
}
|
|
|
|
type app struct {
|
|
allTests []*components.IntegrationTest
|
|
filteredTests []*components.IntegrationTest
|
|
itemIdx int
|
|
testDir string
|
|
filtering bool
|
|
g *gocui.Gui
|
|
listView *gocui.View
|
|
editorView *gocui.View
|
|
}
|
|
|
|
func newApp(testDir string) *app {
|
|
return &app{testDir: testDir, allTests: tests.GetTests(utils.GetLazyRootDirectory())}
|
|
}
|
|
|
|
func (self *app) getCurrentTest() *components.IntegrationTest {
|
|
self.adjustCursor()
|
|
if len(self.filteredTests) > 0 {
|
|
return self.filteredTests[self.itemIdx]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *app) loadTests() {
|
|
self.filteredTests = self.allTests
|
|
|
|
self.adjustCursor()
|
|
}
|
|
|
|
func (self *app) adjustCursor() {
|
|
self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1)
|
|
}
|
|
|
|
func (self *app) filterWithString(needle string) {
|
|
if needle == "" {
|
|
self.filteredTests = self.allTests
|
|
} else {
|
|
self.filteredTests = lo.Filter(self.allTests, func(test *components.IntegrationTest, _ int) bool {
|
|
return strings.Contains(test.Name(), needle)
|
|
})
|
|
}
|
|
|
|
self.renderTests()
|
|
self.g.Update(func(g *gocui.Gui) error { return nil })
|
|
}
|
|
|
|
func (self *app) renderTests() {
|
|
self.listView.Clear()
|
|
for _, test := range self.filteredTests {
|
|
fmt.Fprintln(self.listView, test.Name())
|
|
}
|
|
}
|
|
|
|
func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key) bool) func(v *gocui.View, key gocui.Key) bool {
|
|
return func(v *gocui.View, key gocui.Key) bool {
|
|
matched := f(v, key)
|
|
if matched {
|
|
self.filterWithString(v.TextArea.GetContent())
|
|
}
|
|
return matched
|
|
}
|
|
}
|
|
|
|
func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
|
if err := gocui.Screen.Suspend(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
runTuiTest(test, sandbox, waitForDebugger, raceDetector, inputDelay)
|
|
|
|
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return"))
|
|
_, _ = fmt.Scanln() // wait for enter press
|
|
|
|
if err := gocui.Screen.Resume(); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (self *app) layout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
descriptionViewHeight := 7
|
|
keybindingsViewHeight := 3
|
|
editorViewHeight := 3
|
|
if !self.filtering {
|
|
editorViewHeight = 0
|
|
} else {
|
|
descriptionViewHeight = 0
|
|
keybindingsViewHeight = 0
|
|
}
|
|
g.Cursor = self.filtering
|
|
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) {
|
|
return err
|
|
}
|
|
|
|
if self.listView == nil {
|
|
self.listView = listView
|
|
}
|
|
|
|
listView.Highlight = true
|
|
listView.SelBgColor = gocui.ColorBlue
|
|
self.renderTests()
|
|
listView.Title = "Tests"
|
|
listView.FgColor = gocui.ColorDefault
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
|
|
if err != nil {
|
|
if !errors.Is(err, gocui.ErrUnknownView) {
|
|
return err
|
|
}
|
|
descriptionView.Title = "Test description"
|
|
descriptionView.Wrap = true
|
|
descriptionView.FgColor = gocui.ColorDefault
|
|
}
|
|
|
|
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
|
|
if err != nil {
|
|
if !errors.Is(err, gocui.ErrUnknownView) {
|
|
return err
|
|
}
|
|
keybindingsView.Title = "Keybindings"
|
|
keybindingsView.Wrap = true
|
|
keybindingsView.FgColor = gocui.ColorDefault
|
|
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, d: debug test, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
|
|
}
|
|
|
|
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
|
|
if err != nil {
|
|
if !errors.Is(err, gocui.ErrUnknownView) {
|
|
return err
|
|
}
|
|
|
|
if self.editorView == nil {
|
|
self.editorView = editorView
|
|
}
|
|
|
|
editorView.Title = "Filter"
|
|
editorView.FgColor = gocui.ColorDefault
|
|
editorView.Editable = true
|
|
editorView.Editor = gocui.EditorFunc(self.wrapEditor(gocui.SimpleEditor))
|
|
}
|
|
|
|
currentTest := self.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
descriptionView.Clear()
|
|
fmt.Fprint(descriptionView, currentTest.Description())
|
|
|
|
return nil
|
|
}
|
|
|
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
|
err := components.RunTests(components.RunTestArgs{
|
|
Tests: []*components.IntegrationTest{test},
|
|
Logf: log.Printf,
|
|
RunCmd: runCmdInTerminal,
|
|
TestWrapper: runAndPrintError,
|
|
Sandbox: sandbox,
|
|
WaitForDebugger: waitForDebugger,
|
|
RaceDetector: raceDetector,
|
|
CodeCoverageDir: "",
|
|
InputDelay: inputDelay,
|
|
MaxAttempts: 1,
|
|
})
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
}
|
|
|
|
func runAndPrintError(test *components.IntegrationTest, f func() error) {
|
|
if err := f(); err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
}
|