Merge branch 'master' into stash-untracked-changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
@@ -192,7 +192,7 @@ func (self *MergeConflictsController) HandleUndo() error {
|
||||
|
||||
self.c.LogAction("Restoring file to previous state")
|
||||
self.c.LogCommand("Undoing last conflict resolution", false)
|
||||
if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0o644); err != nil {
|
||||
if err := os.WriteFile(state.GetPath(), []byte(state.GetContent()), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ func (self *MergeConflictsController) resolveConflict(selection mergeconflicts.S
|
||||
self.c.LogAction("Resolve merge conflict")
|
||||
self.c.LogCommand(logStr, false)
|
||||
state.PushContent(content)
|
||||
return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0o644)
|
||||
return true, os.WriteFile(state.GetPath(), []byte(content), 0o644)
|
||||
}
|
||||
|
||||
func (self *MergeConflictsController) onLastConflictResolved() error {
|
||||
|
||||
@@ -22,9 +22,13 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch
|
||||
textArea.MoveCursorDown()
|
||||
case key == gocui.KeyArrowUp:
|
||||
textArea.MoveCursorUp()
|
||||
case key == gocui.KeyArrowLeft:
|
||||
case key == gocui.KeyArrowLeft && (mod&gocui.ModAlt) != 0:
|
||||
textArea.MoveLeftWord()
|
||||
case key == gocui.KeyArrowLeft || key == gocui.KeyCtrlB:
|
||||
textArea.MoveCursorLeft()
|
||||
case key == gocui.KeyArrowRight:
|
||||
case key == gocui.KeyArrowRight && (mod&gocui.ModAlt) != 0:
|
||||
textArea.MoveRightWord()
|
||||
case key == gocui.KeyArrowRight || key == gocui.KeyCtrlF:
|
||||
textArea.MoveCursorRight()
|
||||
case key == newlineKey:
|
||||
if allowMultiline {
|
||||
@@ -38,10 +42,16 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch
|
||||
textArea.ToggleOverwrite()
|
||||
case key == gocui.KeyCtrlU:
|
||||
textArea.DeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlK:
|
||||
textArea.DeleteToEndOfLine()
|
||||
case key == gocui.KeyCtrlA || key == gocui.KeyHome:
|
||||
textArea.GoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE || key == gocui.KeyEnd:
|
||||
textArea.GoToEndOfLine()
|
||||
case key == gocui.KeyCtrlW:
|
||||
textArea.BackSpaceWord()
|
||||
case key == gocui.KeyCtrlY:
|
||||
textArea.Yank()
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
|
||||
@@ -2,7 +2,7 @@ package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -185,7 +185,7 @@ type GuiRepoState struct {
|
||||
// WindowViewNameMap is a mapping of windows to the current view of that window.
|
||||
// Some views move between windows for example the commitFiles view and when cycling through
|
||||
// side windows we need to know which view to give focus to for a given window
|
||||
WindowViewNameMap map[string]string
|
||||
WindowViewNameMap *utils.ThreadSafeMap[string, string]
|
||||
|
||||
// tells us whether we've set up our views for the current repo. We'll need to
|
||||
// do this whenever we switch back and forth between repos to get the views
|
||||
@@ -494,8 +494,6 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
})
|
||||
deadlock.Opts.Disable = !gui.Debug
|
||||
|
||||
gui.handleTestMode(startArgs.IntegrationTest)
|
||||
|
||||
gui.g.OnSearchEscape = gui.onSearchEscape
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
return nil
|
||||
@@ -552,6 +550,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
|
||||
gui.c.Log.Info("starting main loop")
|
||||
|
||||
gui.handleTestMode(startArgs.IntegrationTest)
|
||||
|
||||
return gui.g.MainLoop()
|
||||
}
|
||||
|
||||
@@ -656,13 +656,16 @@ func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error { //nolint:unpara
|
||||
|
||||
err := subprocess.Run()
|
||||
|
||||
subprocess.Stdout = ioutil.Discard
|
||||
subprocess.Stderr = ioutil.Discard
|
||||
subprocess.Stdout = io.Discard
|
||||
subprocess.Stderr = io.Discard
|
||||
subprocess.Stdin = nil
|
||||
|
||||
if gui.Config.GetUserConfig().PromptToReturnFromSubprocess {
|
||||
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint(gui.Tr.PressEnterToReturn))
|
||||
fmt.Scanln() // wait for enter press
|
||||
|
||||
// scan to buffer to prevent run unintentional operations when TUI resumes.
|
||||
var buffer string
|
||||
fmt.Scanln(&buffer) // wait for enter press
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -79,3 +79,11 @@ func (self *GuiDriver) MainView() *gocui.View {
|
||||
func (self *GuiDriver) SecondaryView() *gocui.View {
|
||||
return self.gui.secondaryView()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) View(viewName string) *gocui.View {
|
||||
view, err := self.gui.g.View(viewName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -27,11 +27,35 @@ func (gui *Gui) runTaskForView(view *gocui.View, task types.UpdateTask) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) moveMainContextPairToTop(pair types.MainContextPair) {
|
||||
gui.setWindowContext(pair.Main)
|
||||
gui.moveToTopOfWindow(pair.Main)
|
||||
gui.moveMainContextToTop(pair.Main)
|
||||
if pair.Secondary != nil {
|
||||
gui.setWindowContext(pair.Secondary)
|
||||
gui.moveToTopOfWindow(pair.Secondary)
|
||||
gui.moveMainContextToTop(pair.Secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) moveMainContextToTop(context types.Context) {
|
||||
gui.setWindowContext(context)
|
||||
|
||||
view := context.GetView()
|
||||
|
||||
topView := gui.topViewInWindow(context.GetWindowName())
|
||||
if topView == nil {
|
||||
gui.Log.Error("unexpected: topView is nil")
|
||||
return
|
||||
}
|
||||
|
||||
if topView != view {
|
||||
// We need to copy the content to avoid a flicker effect: If we're flicking
|
||||
// through files in the files panel, we use a different view to render the
|
||||
// files vs the directories, and if you select dir A, then file B, then dir
|
||||
// C, you'll briefly see dir A's contents again before the view is updated.
|
||||
// So here we're copying the content from the top window to avoid that
|
||||
// flicker effect.
|
||||
gui.g.CopyContent(topView, view)
|
||||
|
||||
if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,17 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
||||
{
|
||||
isActive: gui.State.Modes.CherryPicking.Active,
|
||||
description: func() string {
|
||||
copiedCount := len(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
text := gui.c.Tr.LcCommitsCopied
|
||||
if copiedCount == 1 {
|
||||
text = gui.c.Tr.LcCommitCopied
|
||||
}
|
||||
|
||||
return gui.withResetButton(
|
||||
fmt.Sprintf(
|
||||
"%d commits copied",
|
||||
len(gui.State.Modes.CherryPicking.CherryPickedCommits),
|
||||
"%d %s",
|
||||
copiedCount,
|
||||
text,
|
||||
),
|
||||
style.FgCyan,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
|
||||
func (gui *Gui) getCurrentBranch(path string) string {
|
||||
readHeadFile := func(path string) (string, error) {
|
||||
headFile, err := ioutil.ReadFile(filepath.Join(path, "HEAD"))
|
||||
headFile, err := os.ReadFile(filepath.Join(path, "HEAD"))
|
||||
if err == nil {
|
||||
content := strings.TrimSpace(string(headFile))
|
||||
refsPrefix := "ref: refs/heads/"
|
||||
@@ -47,7 +46,7 @@ func (gui *Gui) getCurrentBranch(path string) string {
|
||||
}
|
||||
} else {
|
||||
// worktree
|
||||
if worktreeGitDir, err := ioutil.ReadFile(gitDirPath); err == nil {
|
||||
if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil {
|
||||
content := strings.TrimSpace(string(worktreeGitDir))
|
||||
worktreePath := strings.TrimPrefix(content, "gitdir: ")
|
||||
if branch, err := readHeadFile(worktreePath); err == nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package custom_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
@@ -45,8 +46,9 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
|
||||
return func() error {
|
||||
sessionState := self.sessionStateLoader.call()
|
||||
promptResponses := make([]string, len(customCommand.Prompts))
|
||||
form := make(map[string]string)
|
||||
|
||||
f := func() error { return self.finalHandler(customCommand, sessionState, promptResponses) }
|
||||
f := func() error { return self.finalHandler(customCommand, sessionState, promptResponses, form) }
|
||||
|
||||
// if we have prompts we'll recursively wrap our confirm handlers with more prompts
|
||||
// until we reach the actual command
|
||||
@@ -60,10 +62,11 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
|
||||
|
||||
wrappedF := func(response string) error {
|
||||
promptResponses[idx] = response
|
||||
form[prompt.Key] = response
|
||||
return g()
|
||||
}
|
||||
|
||||
resolveTemplate := self.getResolveTemplateFn(promptResponses, sessionState)
|
||||
resolveTemplate := self.getResolveTemplateFn(form, promptResponses, sessionState)
|
||||
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
@@ -154,19 +157,25 @@ func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPr
|
||||
type CustomCommandObjects struct {
|
||||
*SessionState
|
||||
PromptResponses []string
|
||||
Form map[string]string
|
||||
}
|
||||
|
||||
func (self *HandlerCreator) getResolveTemplateFn(promptResponses []string, sessionState *SessionState) func(string) (string, error) {
|
||||
func (self *HandlerCreator) getResolveTemplateFn(form map[string]string, promptResponses []string, sessionState *SessionState) func(string) (string, error) {
|
||||
objects := CustomCommandObjects{
|
||||
SessionState: sessionState,
|
||||
PromptResponses: promptResponses,
|
||||
Form: form,
|
||||
}
|
||||
|
||||
return func(templateStr string) (string, error) { return utils.ResolveTemplate(templateStr, objects) }
|
||||
funcs := template.FuncMap{
|
||||
"quote": self.os.Quote,
|
||||
}
|
||||
|
||||
return func(templateStr string) (string, error) { return utils.ResolveTemplate(templateStr, objects, funcs) }
|
||||
}
|
||||
|
||||
func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, sessionState *SessionState, promptResponses []string) error {
|
||||
resolveTemplate := self.getResolveTemplateFn(promptResponses, sessionState)
|
||||
func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, sessionState *SessionState, promptResponses []string, form map[string]string) error {
|
||||
resolveTemplate := self.getResolveTemplateFn(form, promptResponses, sessionState)
|
||||
cmdStr, err := resolveTemplate(customCommand.Command)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
@@ -110,17 +107,3 @@ type CustomCommandObject struct {
|
||||
PromptResponses []string
|
||||
Form map[string]string
|
||||
}
|
||||
|
||||
func ResolveTemplate(templateStr string, object interface{}) (string, error) {
|
||||
tmpl, err := template.New("template").Parse(templateStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, object); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -42,7 +41,7 @@ func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) {
|
||||
if Replaying() {
|
||||
gui.g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: GetRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
Leeway: 1000,
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -93,7 +92,7 @@ func GetRecordingSpeed() float64 {
|
||||
func LoadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,5 +119,5 @@ func SaveRecording(recording *gocui.Recording) error {
|
||||
|
||||
path := recordEventsTo()
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0o600)
|
||||
return os.WriteFile(path, jsonEvents, 0o600)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -14,18 +16,18 @@ import (
|
||||
// space. Right now most windows are 1:1 with views, except for commitFiles which
|
||||
// is a view that moves between windows
|
||||
|
||||
func (gui *Gui) initialWindowViewNameMap(contextTree *context.ContextTree) map[string]string {
|
||||
result := map[string]string{}
|
||||
func (gui *Gui) initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] {
|
||||
result := utils.NewThreadSafeMap[string, string]()
|
||||
|
||||
for _, context := range contextTree.Flatten() {
|
||||
result[context.GetWindowName()] = context.GetViewName()
|
||||
result.Set(context.GetWindowName(), context.GetViewName())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getViewNameForWindow(window string) string {
|
||||
viewName, ok := gui.State.WindowViewNameMap[window]
|
||||
viewName, ok := gui.State.WindowViewNameMap.Get(window)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Viewname not found for window: %s", window))
|
||||
}
|
||||
@@ -50,7 +52,7 @@ func (gui *Gui) setWindowContext(c types.Context) {
|
||||
gui.resetWindowContext(c)
|
||||
}
|
||||
|
||||
gui.State.WindowViewNameMap[c.GetWindowName()] = c.GetViewName()
|
||||
gui.State.WindowViewNameMap.Set(c.GetWindowName(), c.GetViewName())
|
||||
}
|
||||
|
||||
func (gui *Gui) currentWindow() string {
|
||||
@@ -59,39 +61,57 @@ func (gui *Gui) currentWindow() string {
|
||||
|
||||
// assumes the context's windowName has been set to the new window if necessary
|
||||
func (gui *Gui) resetWindowContext(c types.Context) {
|
||||
for windowName, viewName := range gui.State.WindowViewNameMap {
|
||||
for _, windowName := range gui.State.WindowViewNameMap.Keys() {
|
||||
viewName, ok := gui.State.WindowViewNameMap.Get(windowName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if viewName == c.GetViewName() && windowName != c.GetWindowName() {
|
||||
for _, context := range gui.State.Contexts.Flatten() {
|
||||
if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName {
|
||||
gui.State.WindowViewNameMap[windowName] = context.GetViewName()
|
||||
gui.State.WindowViewNameMap.Set(windowName, context.GetViewName())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) moveToTopOfWindow(context types.Context) {
|
||||
// moves given context's view to the top of the window and returns
|
||||
// true if the view was not already on top.
|
||||
func (gui *Gui) moveToTopOfWindow(context types.Context) bool {
|
||||
view := context.GetView()
|
||||
if view == nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
window := context.GetWindowName()
|
||||
|
||||
topView := gui.topViewInWindow(window)
|
||||
|
||||
if view.Name() == topView.Name() {
|
||||
return false
|
||||
} else {
|
||||
if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) topViewInWindow(windowName string) *gocui.View {
|
||||
// now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list.
|
||||
viewNamesInWindow := gui.viewNamesInWindow(window)
|
||||
viewNamesInWindow := gui.viewNamesInWindow(windowName)
|
||||
|
||||
// The views list is ordered highest-last, so we're grabbing the last view of the window
|
||||
topView := view
|
||||
var topView *gocui.View
|
||||
for _, currentView := range gui.g.Views() {
|
||||
if lo.Contains(viewNamesInWindow, currentView.Name()) {
|
||||
topView = currentView
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
return topView
|
||||
}
|
||||
|
||||
func (gui *Gui) viewNamesInWindow(windowName string) []string {
|
||||
|
||||
Reference in New Issue
Block a user