ronantakizawa/codereview-bench huggingface dataset:
Repository: jesseduffield/lazygit
File: pkg/app/daemon/daemon.go (Go)

Code before:
func ToEnvVars(instruction Instruction) []string {
	return []string{
		fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
		fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
	}
}

type ExitImmediatelyInstruction struct{}

func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
	return DaemonKindExitImmediately
}

func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
	return serializeInstruction(self)
}

func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
	return nil
}

func NewExitImmediatelyInstruction() Instruction {
	return &ExitImmediatelyInstruction{}
}

type FixTodosFileInstruction struct{}

func (self *FixTodosFileInstruction) Kind() DaemonKind {
	return DaemonKindFixTodosFile
}

func (self *FixTodosFileInstruction) SerializedInstructions() string {
	return serializeInstruction(self)
}

func (self *FixTodosFileInstruction) run(common *common.Common) error {
	return handleInteractiveRebase(common, func(path string) error {
		return nil
	})
}

func NewFixTodosFileInstruction() Instruction {
	return &FixTodosFileInstruction{}
}

type CherryPickCommitsInstruction struct {
	Todo string
}

func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
	todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {

Review comment (bug):
I'd rather call this what it is i.e. `RemoveUpdateRefsForCopiedBranch`. I don't consider the default behaviour broken and in need of fixing; rather I consider it a default that we don't like.

Code after:
func ToEnvVars(instruction Instruction) []string {
	return []string{
		fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
		fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
	}
}

type ExitImmediatelyInstruction struct{}

func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
	return DaemonKindExitImmediately
}

func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
	return serializeInstruction(self)
}

func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
	return nil
}

func NewExitImmediatelyInstruction() Instruction {
	return &ExitImmediatelyInstruction{}
}

type RemoveUpdateRefsForCopiedBranchInstruction struct{}

func (self *RemoveUpdateRefsForCopiedBranchInstruction) Kind() DaemonKind {
	return DaemonKindRemoveUpdateRefsForCopiedBranch
}

func (self *RemoveUpdateRefsForCopiedBranchInstruction) SerializedInstructions() string {
	return serializeInstruction(self)
}

func (self *RemoveUpdateRefsForCopiedBranchInstruction) run(common *common.Common) error {
	return handleInteractiveRebase(common, func(path string) error {
		return nil
	})
}

func NewRemoveUpdateRefsForCopiedBranchInstruction() Instruction {
	return &RemoveUpdateRefsForCopiedBranchInstruction{}
}

type CherryPickCommitsInstruction struct {
	Todo string
}

func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
	todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
Repository: jesseduffield/lazygit
File: pkg/commands/git_commands/commit.go (Go)

Code before:

// CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(hash string) error {
	cmdArgs := NewGitCmd("commit").Arg("--fixup=" + hash).ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// CreateAmendCommit creates a commit that changes the commit message of a previous commit
func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDescription string, includeFileChanges bool) error {
	description := newSubject
	if newDescription != "" {
		description += "\n\n" + newDescription
	}
	cmdArgs := NewGitCmd("commit").
		Arg("-m", "amend! "+originalSubject).
		Arg("-m", description).
		ArgIf(!includeFileChanges, "--only", "--allow-empty").
		ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

func (self *CommitCommands) CreateSquashCommit(selectedBranch, checkedOutBranch string) error {
	cmdArgs := NewGitCmd("commit").
		Arg("-m", fmt.Sprintf("Squash branch %s into %s", selectedBranch, checkedOutBranch)).
		ToArgv()
	return self.cmd.New(cmdArgs).Run()
}

// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
	cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
		ToArgv()

	hash, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
	formattedHash := strings.TrimSpace(hash)
	if len(formattedHash) == 0 {
		return "", ErrInvalidCommitIndex
	}
	return self.GetCommitMessage(formattedHash)
}


Review comment (suggestion):
```suggestion
		Arg("-m", fmt.Sprintf("Squash merge branch %s into %s", selectedBranch, checkedOutBranch)).
```



Code after:

// CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(hash string) error {
	cmdArgs := NewGitCmd("commit").Arg("--fixup=" + hash).ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// CreateAmendCommit creates a commit that changes the commit message of a previous commit
func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDescription string, includeFileChanges bool) error {
	description := newSubject
	if newDescription != "" {
		description += "\n\n" + newDescription
	}
	cmdArgs := NewGitCmd("commit").
		Arg("-m", "amend! "+originalSubject).
		Arg("-m", description).
		ArgIf(!includeFileChanges, "--only", "--allow-empty").
		ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
	cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
		ToArgv()

	hash, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
	formattedHash := strings.TrimSpace(hash)
	if len(formattedHash) == 0 {
		return "", ErrInvalidCommitIndex
	}
	return self.GetCommitMessage(formattedHash)
}
Repository: jesseduffield/lazygit
File: pkg/commands/git_commands/commit.go (Go)

Code before:
	return self.cmd.New(cmdArgs).Run()
}

// CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(hash string) error {
	cmdArgs := NewGitCmd("commit").Arg("--fixup=" + hash).ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// CreateAmendCommit creates a commit that changes the commit message of a previous commit
func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDescription string, includeFileChanges bool) error {
	description := newSubject
	if newDescription != "" {
		description += "\n\n" + newDescription
	}
	cmdArgs := NewGitCmd("commit").
		Arg("-m", "amend! "+originalSubject).
		Arg("-m", description).
		ArgIf(!includeFileChanges, "--only", "--allow-empty").
		ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

func (self *CommitCommands) CreateSquashCommit(selectedBranch, checkedOutBranch string) error {
	cmdArgs := NewGitCmd("commit").
		Arg("-m", fmt.Sprintf("Squash branch %s into %s", selectedBranch, checkedOutBranch)).
		ToArgv()
	return self.cmd.New(cmdArgs).Run()
}

// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
	cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
		ToArgv()

	hash, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
	formattedHash := strings.TrimSpace(hash)
	if len(formattedHash) == 0 {
		return "", ErrInvalidCommitIndex
	}
	return self.GetCommitMessage(formattedHash)
}


Review comment (suggestion):
```suggestion
func (self *CommitCommands) CreateSquashMergeCommit(selectedBranch, checkedOutBranch string) error {
```

Code after:
	return self.cmd.New(cmdArgs).Run()
}

// CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(hash string) error {
	cmdArgs := NewGitCmd("commit").Arg("--fixup=" + hash).ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// CreateAmendCommit creates a commit that changes the commit message of a previous commit
func (self *CommitCommands) CreateAmendCommit(originalSubject, newSubject, newDescription string, includeFileChanges bool) error {
	description := newSubject
	if newDescription != "" {
		description += "\n\n" + newDescription
	}
	cmdArgs := NewGitCmd("commit").
		Arg("-m", "amend! "+originalSubject).
		Arg("-m", description).
		ArgIf(!includeFileChanges, "--only", "--allow-empty").
		ToArgv()

	return self.cmd.New(cmdArgs).Run()
}

// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
	cmdArgs := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
		ToArgv()

	hash, _ := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
	formattedHash := strings.TrimSpace(hash)
	if len(formattedHash) == 0 {
		return "", ErrInvalidCommitIndex
	}
	return self.GetCommitMessage(formattedHash)
}
Repository: jesseduffield/lazygit
File: pkg/gui/controllers/helpers/merge_and_rebase_helper.go (Go)

Code before:
			},
			{
				Label:   self.c.Tr.SquashInCommitTitle,
				OnPress: self.SquashIntoNewCommit(refName, checkedOutBranchName),
				Key:     'S',
				Tooltip: utils.ResolvePlaceholderString(
					self.c.Tr.SquashInCommit,
					map[string]string{
						"checkedOutBranch": checkedOutBranchName,
						"selectedBranch":   refName,
					},
				),
			},
		},
	})
}

func (self *MergeAndRebaseHelper) SquashInFiles(refName string) func() error {
	return func() error {
		self.c.LogAction(self.c.Tr.Actions.SquashBranch)
		err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{Squash: true})
		return self.CheckMergeOrRebase(err)
	}
}

func (self *MergeAndRebaseHelper) SquashIntoNewCommit(refName, checkedOutBranchName string) func() error {
	return func() error {
		self.c.LogAction(self.c.Tr.Actions.SquashBranch)
		err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{Squash: true})
		if err = self.CheckMergeOrRebase(err); err != nil {
			return err
		}
		err = self.c.Git().Commit.CreateSquashCommit(refName, checkedOutBranchName)
		if err != nil {
			return err
		}
		return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
	}
}

func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error {
	self.c.Modes().MarkedBaseCommit.Reset()
	return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
}


Review comment (refactor):
```suggestion
func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranchName string) func() error {
```

Code after:
					}
					return err
				})
			},
		},
	}

	title := utils.ResolvePlaceholderString(
		lo.Ternary(self.c.Modes().MarkedBaseCommit.GetHash() != "",
			self.c.Tr.RebasingFromBaseCommitTitle,
			self.c.Tr.RebasingTitle),
		map[string]string{
			"checkedOutBranch": checkedOutBranchName,
		},
	)

	return self.c.Menu(types.CreateMenuOptions{
		Title: title,
		Items: menuItems,
	})
}

func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error {
	if self.c.Git().Branch.IsHeadDetached() {
		return errors.New("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
	}
	checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
	if checkedOutBranchName == refName {
		return errors.New(self.c.Tr.CantMergeBranchIntoItself)
	}

	return self.c.Menu(types.CreateMenuOptions{
		Title: self.c.Tr.Merge,
		Items: []*types.MenuItem{
			{
				Label:   self.c.Tr.RegularMerge,
				OnPress: self.RegularMerge(refName),
				Key:     'm',
				Tooltip: utils.ResolvePlaceholderString(
					self.c.Tr.RegularMergeTooltip,
					map[string]string{
						"checkedOutBranch": checkedOutBranchName,
						"selectedBranch":   refName,
					},
				),
			},
			{
				Label:   self.c.Tr.SquashMergeUncommittedTitle,
				OnPress: self.SquashMergeUncommitted(refName),
				Key:     's',
				Tooltip: utils.ResolvePlaceholderString(
Repository: jesseduffield/lazygit
File: pkg/gui/presentation/commits.go (Go)

Code before:
		name = emoji.Sprint(name)
	}

	mark := ""
	if isYouAreHereCommit {
		color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
		youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
		mark = fmt.Sprintf("%s ", youAreHere)
	} else if isMarkedBaseCommit {
		rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
		mark = fmt.Sprintf("%s ", rebaseFromHere)
	} else if !willBeRebased {
		willBeRebased := style.FgYellow.Sprint("✓")
		mark = fmt.Sprintf("%s ", willBeRebased)
	}

	authorFunc := authors.ShortAuthor
	switch common.UserConfig.Gui.CommitAuthorFormat {
	case "short":
		authorFunc = authors.ShortAuthor
	case "full":
		authorFunc = authors.LongAuthor
	default:
		if fullDescription {
			authorFunc = authors.LongAuthor
		}
	}

	cols := make([]string, 0, 7)
	cols = append(
		cols,
		divergenceString,
		hashString,
		bisectString,
		descriptionString,
		actionString,
		authorFunc(commit.AuthorName),
		graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
	)

	return cols
}

func getBisectStatusColor(status BisectStatus) style.TextStyle {
	switch status {
	case BisectStatusNone:
		return style.FgBlack
	case BisectStatusNew:
		return style.FgRed
	case BisectStatusOld:
		return style.FgGreen

Review comment (security):
One last comment, though. For consistency's sake, let's use the `user_config_validation.go` file to validate the `commitAuthorFormat` setting instead of falling back to the `auto` setting.

Code after:
		name = emoji.Sprint(name)
	}

	mark := ""
	if isYouAreHereCommit {
		color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
		youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
		mark = fmt.Sprintf("%s ", youAreHere)
	} else if isMarkedBaseCommit {
		rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
		mark = fmt.Sprintf("%s ", rebaseFromHere)
	} else if !willBeRebased {
		willBeRebased := style.FgYellow.Sprint("✓")
		mark = fmt.Sprintf("%s ", willBeRebased)
	}

	var authorFunc func(string) string
	switch common.UserConfig.Gui.CommitAuthorFormat {
	case "short":
		authorFunc = authors.ShortAuthor
	case "full":
		authorFunc = authors.LongAuthor
	default:
		authorFunc = lo.Ternary(fullDescription, authors.LongAuthor, authors.ShortAuthor)
	}

	cols := make([]string, 0, 7)
	cols = append(
		cols,
		divergenceString,
		hashString,
		bisectString,
		descriptionString,
		actionString,
		authorFunc(commit.AuthorName),
		graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
	)

	return cols
}

func getBisectStatusColor(status BisectStatus) style.TextStyle {
	switch status {
	case BisectStatusNone:
		return style.FgBlack
	case BisectStatusNew:
		return style.FgRed
	case BisectStatusOld:
		return style.FgGreen
	case BisectStatusSkipped:
		return style.FgYellow
Repository: jesseduffield/lazygit
File: pkg/gui/presentation/commits.go (Go)

Code before:

	name := commit.Name
	if commit.Action == todo.UpdateRef {
		name = strings.TrimPrefix(name, "refs/heads/")
	}
	if parseEmoji {
		name = emoji.Sprint(name)
	}

	mark := ""
	if isYouAreHereCommit {
		color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
		youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
		mark = fmt.Sprintf("%s ", youAreHere)
	} else if isMarkedBaseCommit {
		rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
		mark = fmt.Sprintf("%s ", rebaseFromHere)
	} else if !willBeRebased {
		willBeRebased := style.FgYellow.Sprint("✓")
		mark = fmt.Sprintf("%s ", willBeRebased)
	}

	authorFunc := authors.ShortAuthor
	switch common.UserConfig.Gui.CommitAuthorFormat {
	case "short":
		authorFunc = authors.ShortAuthor
	case "full":
		authorFunc = authors.LongAuthor
	default:
		if fullDescription {
			authorFunc = authors.LongAuthor
		}
	}

	cols := make([]string, 0, 7)
	cols = append(
		cols,
		divergenceString,
		hashString,
		bisectString,
		descriptionString,
		actionString,
		authorFunc(commit.AuthorName),
		graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
	)

	return cols
}

func getBisectStatusColor(status BisectStatus) style.TextStyle {
	switch status {

Review comment (security):
Nitpick: this assignment is a no-op, it has this value already. To make this clearer, we might say
```go
	var authorFunc func(string) string
	switch common.UserConfig.Gui.CommitAuthorFormat {
	case "short":
		authorFunc = authors.ShortAuthor
	case "full":
		authorFunc = authors.LongAuthor
	default:
		authorFunc = lo.Ternary(fullDescription, authors.LongAuthor, authors.ShortAuthor)
	}
```


Code after:

	name := commit.Name
	if commit.Action == todo.UpdateRef {
		name = strings.TrimPrefix(name, "refs/heads/")
	}
	if parseEmoji {
		name = emoji.Sprint(name)
	}

	mark := ""
	if isYouAreHereCommit {
		color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
		youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
		mark = fmt.Sprintf("%s ", youAreHere)
	} else if isMarkedBaseCommit {
		rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
		mark = fmt.Sprintf("%s ", rebaseFromHere)
	} else if !willBeRebased {
		willBeRebased := style.FgYellow.Sprint("✓")
		mark = fmt.Sprintf("%s ", willBeRebased)
	}

	var authorFunc func(string) string
	switch common.UserConfig.Gui.CommitAuthorFormat {
	case "short":
		authorFunc = authors.ShortAuthor
	case "full":
		authorFunc = authors.LongAuthor
	default:
		authorFunc = lo.Ternary(fullDescription, authors.LongAuthor, authors.ShortAuthor)
	}

	cols := make([]string, 0, 7)
	cols = append(
		cols,
		divergenceString,
		hashString,
		bisectString,
		descriptionString,
		actionString,
		authorFunc(commit.AuthorName),
		graphLine+mark+tagString+theme.DefaultTextColor.Sprint(name),
	)

	return cols
}

func getBisectStatusColor(status BisectStatus) style.TextStyle {
	switch status {
	case BisectStatusNone:
		return style.FgBlack
Repository: jesseduffield/lazygit
File: pkg/integration/tests/ui/keybinding_suggestions_when_switching_repos.go (Go)

Code before:

var KeybindingSuggestionsWhenSwitchingRepos = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Show correct keybinding suggestions after switching between repos",
	ExtraCmdArgs: []string{},
	Skip:         false,
	SetupConfig: func(config *config.AppConfig) {
		otherRepo, _ := filepath.Abs("../other")
		config.AppState.RecentRepos = []string{otherRepo}
	},
	SetupRepo: func(shell *Shell) {
		shell.CloneNonBare("other")
	},
	Run: func(t *TestDriver, keys config.KeybindingConfig) {
		switchToRepo := func(repo string) {
			t.GlobalPress(keys.Universal.OpenRecentRepos)
			t.ExpectPopup().Menu().Title(Equals("Recent repositories")).
				Lines(
					Contains(repo).IsSelected(),
					Contains("Cancel"),
				).Confirm()
			t.Views().Status().Content(Contains(repo + " → master"))
		}

		t.Views().Files().Focus()
		t.Views().Options().Content(
			Equals("Commit: c | Stash: s | Reset: D | Keybindings: ? | Cancel: <esc>"))

		switchToRepo("other")
		switchToRepo("repo")

		t.Views().Options().Content(
			/* EXPECTED:
			Equals("Commit: c | Stash: s | Reset: D | Keybindings: ? | Cancel: <esc>"))
			ACTUAL (all keybindings appear twice): */
			Equals("Commit: c | Stash: s | Reset: D | Commit: c | Stash: s | Reset: D | Keybindings: ? | Cancel: <esc> | Keybindings: ? | Cancel: <esc>"))
	},
})


Review comment (suggestion):
It's a little awkward that we depend on the concrete keybinding suggestions here, so we'll have to adapt the test when those change. It doesn't bother me too much though, it's still better than not having a test at all.

I did choose the panel that has the lowest number of keybindings.

Code after:

var KeybindingSuggestionsWhenSwitchingRepos = NewIntegrationTest(NewIntegrationTestArgs{
	Description:  "Show correct keybinding suggestions after switching between repos",
	ExtraCmdArgs: []string{},
	Skip:         false,
	SetupConfig: func(config *config.AppConfig) {
		otherRepo, _ := filepath.Abs("../other")
		config.AppState.RecentRepos = []string{otherRepo}
	},
	SetupRepo: func(shell *Shell) {
		shell.CloneNonBare("other")
	},
	Run: func(t *TestDriver, keys config.KeybindingConfig) {
		switchToRepo := func(repo string) {
			t.GlobalPress(keys.Universal.OpenRecentRepos)
			t.ExpectPopup().Menu().Title(Equals("Recent repositories")).
				Lines(
					Contains(repo).IsSelected(),
					Contains("Cancel"),
				).Confirm()
			t.Views().Status().Content(Contains(repo + " → master"))
		}

		t.Views().Files().Focus()
		t.Views().Options().Content(
			Equals("Commit: c | Stash: s | Reset: D | Keybindings: ? | Cancel: <esc>"))

		switchToRepo("other")
		switchToRepo("repo")

		t.Views().Options().Content(
			Equals("Commit: c | Stash: s | Reset: D | Keybindings: ? | Cancel: <esc>"))
	},
})
Repository: jesseduffield/lazygit
File: pkg/utils/formatting.go (Go)

Code before:
package utils

import (
	"fmt"
	"strings"
	"unicode"

	"github.com/mattn/go-runewidth"
	"github.com/samber/lo"
	"golang.org/x/exp/slices"
)

type Alignment int

const (
	AlignLeft Alignment = iota
	AlignRight
)

type ColumnConfig struct {
	Width     int
	Alignment Alignment
}

func RuneWidth(s string) int {
	// We are intentionally not using a range loop here, because that would
	// convert the characters to runes, which is unnecessary work in this case.
	for i := 0; i < len(s); i++ {
		if s[i] > unicode.MaxASCII {
			return runewidth.StringWidth(s)
		}
	}

	return len(s)
}

// WithPadding pads a string as much as you want
func WithPadding(str string, padding int, alignment Alignment) string {
	uncoloredStr := Decolorise(str)
	width := RuneWidth(uncoloredStr)
	if padding < width {
		return str
	}
	space := strings.Repeat(" ", padding-width)
	if alignment == AlignLeft {
		return str + space
	} else {
		return space + str
	}
}

Review comment (performance):
I was surprised that this performance improvement was so easy to implement, so I took a look at the runewidth repo and there are three open (stale) PRs that improve performance:

* https://github.com/mattn/go-runewidth/pull/78
* https://github.com/mattn/go-runewidth/pull/63
* https://github.com/mattn/go-runewidth/pull/42

Of those https://github.com/mattn/go-runewidth/pull/63 seems the most legit (It includes PR 42's approach, and was made by the guy who made tview).

I'm thinking we should use that fork, what are your thoughts? Seems the maintainer of the main repo is not very responsive.

If we do want to continue with our own function here, I think it ought to be called StringWidth given that it is returning the length of the string.

Code after:
package utils

import (
	"fmt"
	"strings"
	"unicode"

	"github.com/mattn/go-runewidth"
	"github.com/samber/lo"
	"golang.org/x/exp/slices"
)

type Alignment int

const (
	AlignLeft Alignment = iota
	AlignRight
)

type ColumnConfig struct {
	Width     int
	Alignment Alignment
}

func StringWidth(s string) int {
	// We are intentionally not using a range loop here, because that would
	// convert the characters to runes, which is unnecessary work in this case.
	for i := 0; i < len(s); i++ {
		if s[i] > unicode.MaxASCII {
			return runewidth.StringWidth(s)
		}
	}

	return len(s)
}

// WithPadding pads a string as much as you want
func WithPadding(str string, padding int, alignment Alignment) string {
	uncoloredStr := Decolorise(str)
	width := StringWidth(uncoloredStr)
	if padding < width {
		return str
	}
	space := strings.Repeat(" ", padding-width)
	if alignment == AlignLeft {
		return str + space
	} else {
		return space + str
	}
}