1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-18 05:17:55 +02:00
This commit is contained in:
Jesse Duffield 2021-05-30 15:22:04 +10:00
parent bc044c64b2
commit 258eedb38c
15 changed files with 802 additions and 680 deletions

View File

@ -0,0 +1,63 @@
package mergeconflicts
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// LineType tells us whether a given line is a start/middle/end marker of a conflict,
// or if it's not a marker at all
type LineType int
const (
START LineType = iota
MIDDLE
END
NOT_A_MARKER
)
func findConflicts(content string) []*mergeConflict {
conflicts := make([]*mergeConflict, 0)
if content == "" {
return conflicts
}
var newConflict *mergeConflict
for i, line := range utils.SplitLines(content) {
switch determineLineType(line) {
case START:
newConflict = &mergeConflict{start: i}
case MIDDLE:
newConflict.middle = i
case END:
newConflict.end = i
conflicts = append(conflicts, newConflict)
default:
// line isn't a merge conflict marker so we just continue
}
}
return conflicts
}
func determineLineType(line string) LineType {
trimmedLine := strings.TrimPrefix(line, "++")
mapping := map[string]LineType{
"^<<<<<<< (HEAD|MERGE_HEAD|Updated upstream|ours)(:.+)?$": START,
"^=======$": MIDDLE,
"^>>>>>>> .*$": END,
}
for regexp_str, lineType := range mapping {
match, _ := regexp.MatchString(regexp_str, trimmedLine)
if match {
return lineType
}
}
return NOT_A_MARKER
}

View File

@ -0,0 +1,57 @@
package mergeconflicts
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDetermineLineType(t *testing.T) {
type scenario struct {
line string
expected LineType
}
scenarios := []scenario{
{
line: "",
expected: NOT_A_MARKER,
},
{
line: "blah",
expected: NOT_A_MARKER,
},
{
line: "<<<<<<< HEAD",
expected: START,
},
{
line: "<<<<<<< HEAD:my_branch",
expected: START,
},
{
line: "<<<<<<< MERGE_HEAD:my_branch",
expected: START,
},
{
line: "<<<<<<< Updated upstream:my_branch",
expected: START,
},
{
line: "<<<<<<< ours:my_branch",
expected: START,
},
{
line: "=======",
expected: MIDDLE,
},
{
line: ">>>>>>> blah",
expected: END,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, determineLineType(s.line))
}
}

View File

@ -1,9 +1,6 @@
package mergeconflicts
import (
"bufio"
"os"
"strings"
"sync"
"github.com/golang-collections/collections/stack"
@ -19,7 +16,7 @@ const (
)
// mergeConflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear
// numbers in the file where the conflict markers appear
type mergeConflict struct {
start int
middle int
@ -88,36 +85,6 @@ func (s *State) SetConflictsFromCat(cat string) {
s.setConflicts(findConflicts(cat))
}
func findConflicts(content string) []*mergeConflict {
conflicts := make([]*mergeConflict, 0)
if content == "" {
return conflicts
}
var newConflict *mergeConflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
switch trimmedLine {
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
newConflict = &mergeConflict{start: i}
case "=======":
newConflict.middle = i
default:
// Sometimes these lines look like "<<<<<<< HEAD:foo/bar/baz.go" so handle that case as well.
if strings.HasPrefix(trimmedLine, "<<<<<<< HEAD:") {
newConflict = &mergeConflict{start: i}
}
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.end = i
conflicts = append(conflicts, newConflict)
}
}
}
return conflicts
}
func (s *State) setConflicts(conflicts []*mergeConflict) {
s.conflicts = conflicts
@ -158,32 +125,29 @@ func (s *State) ContentAfterConflictResolve(path string, selection Selection) (b
return false, "", nil
}
file, err := os.Open(path)
if err != nil {
return false, "", err
}
defer file.Close()
reader := bufio.NewReader(file)
content := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
err := utils.ForEachLineInFile(path, func(line string, i int) {
if !isIndexToDelete(i, conflict, selection) {
content += line
}
})
if err != nil {
return false, "", err
}
return true, content, nil
}
func isIndexToDelete(i int, conflict *mergeConflict, selection Selection) bool {
return i == conflict.middle ||
isMarkerLine :=
i == conflict.middle ||
i == conflict.start ||
i == conflict.end ||
selection != BOTH &&
(selection == BOTTOM && i > conflict.start && i < conflict.middle) ||
(selection == TOP && i > conflict.middle && i < conflict.end)
i == conflict.end
isUnwantedContent :=
(selection == BOTTOM && conflict.start < i && i < conflict.middle) ||
(selection == TOP && conflict.middle < i && i < conflict.end)
return isMarkerLine || isUnwantedContent
}

49
pkg/utils/color.go Normal file
View File

@ -0,0 +1,49 @@
package utils
import (
"fmt"
"regexp"
"github.com/fatih/color"
)
// ColoredString takes a string and a colour attribute and returns a colored
// string with that attribute
func ColoredString(str string, colorAttributes ...color.Attribute) string {
colour := color.New(colorAttributes...)
return ColoredStringDirect(str, colour)
}
// ColoredStringDirect used for aggregating a few color attributes rather than
// just sending a single one
func ColoredStringDirect(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
func getPadWidths(stringArrays [][]string) []int {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
return padWidths
}

39
pkg/utils/formatting.go Normal file
View File

@ -0,0 +1,39 @@
package utils
import "strings"
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
uncoloredStr := Decolorise(str)
if padding < len(uncoloredStr) {
return str
}
return str + strings.Repeat(" ", padding-len(uncoloredStr))
}
func RenderDisplayStrings(displayStringsArr [][]string) string {
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
return strings.Join(paddedDisplayStrings, "\n")
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
}
for j, padWidth := range padWidths {
if len(stringArray)-1 < j {
continue
}
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
if len(stringArray)-1 < len(padWidths) {
continue
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
}

View File

@ -0,0 +1,81 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestWithPadding is a function.
func TestWithPadding(t *testing.T) {
type scenario struct {
str string
padding int
expected string
}
scenarios := []scenario{
{
"hello world !",
1,
"hello world !",
},
{
"hello world !",
14,
"hello world ! ",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding))
}
}
// TestGetPaddedDisplayStrings is a function.
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
// TestGetPadWidths is a function.
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
}
scenarios := []scenario{
{
[][]string{{""}, {""}},
[]int{},
},
{
[][]string{{"a"}, {""}},
[]int{},
},
{
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
}
}

25
pkg/utils/io.go Normal file
View File

@ -0,0 +1,25 @@
package utils
import (
"bufio"
"os"
)
func ForEachLineInFile(path string, f func(string, int)) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
f(line, i)
}
return nil
}

34
pkg/utils/lines.go Normal file
View File

@ -0,0 +1,34 @@
package utils
import "strings"
// SplitLines takes a multiline string and splits it on newlines
// currently we are also stripping \r's which may have adverse effects for
// windows users (but no issues have been raised yet)
func SplitLines(multilineString string) []string {
multilineString = strings.Replace(multilineString, "\r", "", -1)
if multilineString == "" || multilineString == "\n" {
return make([]string, 0)
}
lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" {
return lines[:len(lines)-1]
}
return lines
}
// TrimTrailingNewline - Trims the trailing newline
// TODO: replace with `chomp` after refactor
func TrimTrailingNewline(str string) string {
if strings.HasSuffix(str, "\n") {
return str[:len(str)-1]
}
return str
}
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
func NormalizeLinefeeds(str string) string {
str = strings.Replace(str, "\r\n", "\n", -1)
str = strings.Replace(str, "\r", "", -1)
return str
}

94
pkg/utils/lines_test.go Normal file
View File

@ -0,0 +1,94 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestSplitLines is a function.
func TestSplitLines(t *testing.T) {
type scenario struct {
multilineString string
expected []string
}
scenarios := []scenario{
{
"",
[]string{},
},
{
"\n",
[]string{},
},
{
"hello world !\nhello universe !\n",
[]string{
"hello world !",
"hello universe !",
},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, SplitLines(s.multilineString))
}
}
// TestTrimTrailingNewline is a function.
func TestTrimTrailingNewline(t *testing.T) {
type scenario struct {
str string
expected string
}
scenarios := []scenario{
{
"hello world !\n",
"hello world !",
},
{
"hello world !",
"hello world !",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
}
}
// TestNormalizeLinefeeds is a function.
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
expected []byte
}
var scenarios = []scenario{
{
// \r\n
[]byte{97, 115, 100, 102, 13, 10},
[]byte{97, 115, 100, 102, 10},
},
{
// bash\r\nblah
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
},
{
// \r
[]byte{97, 115, 100, 102, 13},
[]byte{97, 115, 100, 102},
},
{
// \n
[]byte{97, 115, 100, 102, 10},
[]byte{97, 115, 100, 102, 10},
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}

118
pkg/utils/slice.go Normal file
View File

@ -0,0 +1,118 @@
package utils
// IncludesString if the list contains the string
func IncludesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// IncludesInt if the list contains the Int
func IncludesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// NextIndex returns the index of the element that comes after the given number
func NextIndex(numbers []int, currentNumber int) int {
for index, number := range numbers {
if number > currentNumber {
return index
}
}
return len(numbers) - 1
}
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
func PrevIndex(numbers []int, currentNumber int) int {
end := len(numbers) - 1
for i := end; i >= 0; i-- {
if numbers[i] < currentNumber {
return i
}
}
return 0
}
// UnionInt returns the union of two int arrays
func UnionInt(a, b []int) []int {
m := make(map[int]bool)
for _, item := range a {
m[item] = true
}
for _, item := range b {
if _, ok := m[item]; !ok {
// this does not mutate the original a slice
// though it does mutate the backing array I believe
// but that doesn't matter because if you later want to append to the
// original a it must see that the backing array has been changed
// and create a new one
a = append(a, item)
}
}
return a
}
// DifferenceInt returns the difference of two int arrays
func DifferenceInt(a, b []int) []int {
result := []int{}
m := make(map[int]bool)
for _, item := range b {
m[item] = true
}
for _, item := range a {
if _, ok := m[item]; !ok {
result = append(result, item)
}
}
return result
}
// NextIntInCycle returns the next int in a slice, returning to the first index if we've reached the end
func NextIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
return sl[0]
}
// PrevIntInCycle returns the prev int in a slice, returning to the first index if we've reached the end
func PrevIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}
func StringArraysOverlap(strArrA []string, strArrB []string) bool {
for _, first := range strArrA {
for _, second := range strArrB {
if first == second {
return true
}
}
}
return false
}

135
pkg/utils/slice_test.go Normal file
View File

@ -0,0 +1,135 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestIncludesString is a function.
func TestIncludesString(t *testing.T) {
type scenario struct {
list []string
element string
expected bool
}
scenarios := []scenario{
{
[]string{"a", "b"},
"a",
true,
},
{
[]string{"a", "b"},
"c",
false,
},
{
[]string{"a", "b"},
"",
false,
},
{
[]string{""},
"",
true,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
}
}
func TestNextIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
-1,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"two elements, giving second one",
[]int{1, 2},
2,
1,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
2,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
})
}
}
func TestPrevIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
0,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
0,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
0,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
})
}
}

30
pkg/utils/template.go Normal file
View File

@ -0,0 +1,30 @@
package utils
import (
"bytes"
"strings"
"text/template"
)
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
}
// ResolvePlaceholderString populates a template with values
func ResolvePlaceholderString(str string, arguments map[string]string) string {
for key, value := range arguments {
str = strings.Replace(str, "{{"+key+"}}", value, -1)
str = strings.Replace(str, "{{."+key+"}}", value, -1)
}
return str
}

View File

@ -0,0 +1,61 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestResolvePlaceholderString is a function.
func TestResolvePlaceholderString(t *testing.T) {
type scenario struct {
templateString string
arguments map[string]string
expected string
}
scenarios := []scenario{
{
"",
map[string]string{},
"",
},
{
"hello",
map[string]string{},
"hello",
},
{
"hello {{arg}}",
map[string]string{},
"hello {{arg}}",
},
{
"hello {{arg}}",
map[string]string{"arg": "there"},
"hello there",
},
{
"hello",
map[string]string{"arg": "there"},
"hello",
},
{
"{{nothing}}",
map[string]string{"nothing": ""},
"",
},
{
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
map[string]string{
"blah": "blah",
"this": "won't match",
},
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, ResolvePlaceholderString(s.templateString, s.arguments))
}
}

View File

@ -1,7 +1,6 @@
package utils
import (
"bytes"
"encoding/json"
"fmt"
"log"
@ -11,50 +10,11 @@ import (
"runtime"
"strconv"
"strings"
"text/template"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
// SplitLines takes a multiline string and splits it on newlines
// currently we are also stripping \r's which may have adverse effects for
// windows users (but no issues have been raised yet)
func SplitLines(multilineString string) []string {
multilineString = strings.Replace(multilineString, "\r", "", -1)
if multilineString == "" || multilineString == "\n" {
return make([]string, 0)
}
lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" {
return lines[:len(lines)-1]
}
return lines
}
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
uncoloredStr := Decolorise(str)
if padding < len(uncoloredStr) {
return str
}
return str + strings.Repeat(" ", padding-len(uncoloredStr))
}
// ColoredString takes a string and a colour attribute and returns a colored
// string with that attribute
func ColoredString(str string, colorAttributes ...color.Attribute) string {
colour := color.New(colorAttributes...)
return ColoredStringDirect(str, colour)
}
// ColoredStringDirect used for aggregating a few color attributes rather than
// just sending a single one
func ColoredStringDirect(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
// GetCurrentRepoName gets the repo's base name
func GetCurrentRepoName() string {
pwd, err := os.Getwd()
@ -64,22 +24,6 @@ func GetCurrentRepoName() string {
return filepath.Base(pwd)
}
// TrimTrailingNewline - Trims the trailing newline
// TODO: replace with `chomp` after refactor
func TrimTrailingNewline(str string) string {
if strings.HasSuffix(str, "\n") {
return str[:len(str)-1]
}
return str
}
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
func NormalizeLinefeeds(str string) string {
str = strings.Replace(str, "\r\n", "\n", -1)
str = strings.Replace(str, "\r", "", -1)
return str
}
// GetProjectRoot returns the path to the root of the project. Only to be used
// in testing contexts, as with binaries it's unlikely this path will exist on
// the machine
@ -100,15 +44,6 @@ func Loader() string {
return characters[index : index+1]
}
// ResolvePlaceholderString populates a template with values
func ResolvePlaceholderString(str string, arguments map[string]string) string {
for key, value := range arguments {
str = strings.Replace(str, "{{"+key+"}}", value, -1)
str = strings.Replace(str, "{{."+key+"}}", value, -1)
}
return str
}
// Min returns the minimum of two integers
func Min(x, y int) int {
if x < y {
@ -117,156 +52,11 @@ func Min(x, y int) int {
return y
}
func RenderDisplayStrings(displayStringsArr [][]string) string {
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
return strings.Join(paddedDisplayStrings, "\n")
}
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
func getPadWidths(stringArrays [][]string) []int {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
return padWidths
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
}
for j, padWidth := range padWidths {
if len(stringArray)-1 < j {
continue
}
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
if len(stringArray)-1 < len(padWidths) {
continue
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
}
// displayArraysAligned returns true if every string array returned from our
// list of displayables has the same length
func displayArraysAligned(stringArrays [][]string) bool {
for _, strings := range stringArrays {
if len(strings) != len(stringArrays[0]) {
return false
}
}
return true
}
// IncludesString if the list contains the string
func IncludesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// IncludesInt if the list contains the Int
func IncludesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// NextIndex returns the index of the element that comes after the given number
func NextIndex(numbers []int, currentNumber int) int {
for index, number := range numbers {
if number > currentNumber {
return index
}
}
return len(numbers) - 1
}
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
func PrevIndex(numbers []int, currentNumber int) int {
end := len(numbers) - 1
for i := end; i >= 0; i-- {
if numbers[i] < currentNumber {
return i
}
}
return 0
}
func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes)
}
// UnionInt returns the union of two int arrays
func UnionInt(a, b []int) []int {
m := make(map[int]bool)
for _, item := range a {
m[item] = true
}
for _, item := range b {
if _, ok := m[item]; !ok {
// this does not mutate the original a slice
// though it does mutate the backing array I believe
// but that doesn't matter because if you later want to append to the
// original a it must see that the backing array has been changed
// and create a new one
a = append(a, item)
}
}
return a
}
// DifferenceInt returns the difference of two int arrays
func DifferenceInt(a, b []int) []int {
result := []int{}
m := make(map[int]bool)
for _, item := range b {
m[item] = true
}
for _, item := range a {
if _, ok := m[item]; !ok {
result = append(result, item)
}
}
return result
}
// used to keep a number n between 0 and max, allowing for wraparounds
func ModuloWithWrap(n, max int) int {
if n >= max {
@ -278,32 +68,6 @@ func ModuloWithWrap(n, max int) int {
}
}
// NextIntInCycle returns the next int in a slice, returning to the first index if we've reached the end
func NextIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
return sl[0]
}
// PrevIntInCycle returns the prev int in a slice, returning to the first index if we've reached the end
func PrevIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
func TruncateWithEllipsis(str string, limit int) string {
if limit == 1 && len(str) > 1 {
@ -329,18 +93,6 @@ func FindStringSubmatch(str string, regexpStr string) (bool, []string) {
return len(match) > 0, match
}
func StringArraysOverlap(strArrA []string, strArrB []string) bool {
for _, first := range strArrA {
for _, second := range strArrB {
if first == second {
return true
}
}
}
return false
}
func MustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
@ -349,20 +101,6 @@ func MustConvertToInt(s string) int {
return i
}
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
}
// Safe will close tcell if a panic occurs so that we don't end up in a malformed
// terminal state
func Safe(f func()) {

View File

@ -6,244 +6,6 @@ import (
"github.com/stretchr/testify/assert"
)
// TestSplitLines is a function.
func TestSplitLines(t *testing.T) {
type scenario struct {
multilineString string
expected []string
}
scenarios := []scenario{
{
"",
[]string{},
},
{
"\n",
[]string{},
},
{
"hello world !\nhello universe !\n",
[]string{
"hello world !",
"hello universe !",
},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, SplitLines(s.multilineString))
}
}
// TestWithPadding is a function.
func TestWithPadding(t *testing.T) {
type scenario struct {
str string
padding int
expected string
}
scenarios := []scenario{
{
"hello world !",
1,
"hello world !",
},
{
"hello world !",
14,
"hello world ! ",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding))
}
}
// TestTrimTrailingNewline is a function.
func TestTrimTrailingNewline(t *testing.T) {
type scenario struct {
str string
expected string
}
scenarios := []scenario{
{
"hello world !\n",
"hello world !",
},
{
"hello world !",
"hello world !",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
}
}
// TestNormalizeLinefeeds is a function.
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
expected []byte
}
var scenarios = []scenario{
{
// \r\n
[]byte{97, 115, 100, 102, 13, 10},
[]byte{97, 115, 100, 102, 10},
},
{
// bash\r\nblah
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
},
{
// \r
[]byte{97, 115, 100, 102, 13},
[]byte{97, 115, 100, 102},
},
{
// \n
[]byte{97, 115, 100, 102, 10},
[]byte{97, 115, 100, 102, 10},
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}
// TestResolvePlaceholderString is a function.
func TestResolvePlaceholderString(t *testing.T) {
type scenario struct {
templateString string
arguments map[string]string
expected string
}
scenarios := []scenario{
{
"",
map[string]string{},
"",
},
{
"hello",
map[string]string{},
"hello",
},
{
"hello {{arg}}",
map[string]string{},
"hello {{arg}}",
},
{
"hello {{arg}}",
map[string]string{"arg": "there"},
"hello there",
},
{
"hello",
map[string]string{"arg": "there"},
"hello",
},
{
"{{nothing}}",
map[string]string{"nothing": ""},
"",
},
{
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
map[string]string{
"blah": "blah",
"this": "won't match",
},
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, ResolvePlaceholderString(s.templateString, s.arguments))
}
}
// TestDisplayArraysAligned is a function.
func TestDisplayArraysAligned(t *testing.T) {
type scenario struct {
input [][]string
expected bool
}
scenarios := []scenario{
{
[][]string{{"", ""}, {"", ""}},
true,
},
{
[][]string{{""}, {"", ""}},
false,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
}
}
// TestGetPaddedDisplayStrings is a function.
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
// TestGetPadWidths is a function.
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
}
scenarios := []scenario{
{
[][]string{{""}, {""}},
[]int{},
},
{
[][]string{{"a"}, {""}},
[]int{},
},
{
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
}
}
// TestMin is a function.
func TestMin(t *testing.T) {
type scenario struct {
@ -275,134 +37,6 @@ func TestMin(t *testing.T) {
}
}
// TestIncludesString is a function.
func TestIncludesString(t *testing.T) {
type scenario struct {
list []string
element string
expected bool
}
scenarios := []scenario{
{
[]string{"a", "b"},
"a",
true,
},
{
[]string{"a", "b"},
"c",
false,
},
{
[]string{"a", "b"},
"",
false,
},
{
[]string{""},
"",
true,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
}
}
func TestNextIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
-1,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"two elements, giving second one",
[]int{1, 2},
2,
1,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
2,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
})
}
}
func TestPrevIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
0,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
0,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
0,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
})
}
}
func TestAsJson(t *testing.T) {
type myStruct struct {
a string