From 8016244dbb3ae90093ea1e2a04f4a2c4db722ca1 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 28 Aug 2023 06:08:43 -0700 Subject: [PATCH] Use go-gh prompter package (#7896) --- go.mod | 11 +- go.sum | 27 +- internal/prompter/prompter.go | 166 ++++--------- internal/prompter/prompter_test.go | 78 ------ internal/prompter/test.go | 385 ++++++++++------------------- internal/text/text.go | 17 +- internal/text/text_test.go | 54 ---- pkg/iostreams/iostreams.go | 21 +- 8 files changed, 207 insertions(+), 552 deletions(-) delete mode 100644 internal/prompter/prompter_test.go diff --git a/go.mod b/go.mod index 48632a1e3..347f4a5d2 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da + github.com/charmbracelet/glamour v0.6.0 github.com/charmbracelet/lipgloss v0.5.0 - github.com/cli/go-gh/v2 v2.1.0 + github.com/cli/go-gh/v2 v2.3.0 github.com/cli/oauth v1.0.1 github.com/cli/safeexec v1.0.1 github.com/cpuguy83/go-md2man/v2 v2.0.2 @@ -49,6 +49,7 @@ require ( require ( github.com/alecthomas/chroma v0.10.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect + github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cli/browser v1.2.0 // indirect github.com/cli/shurcooL-graphql v0.0.3 // indirect @@ -68,9 +69,9 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/microcosm-cc/bluemonday v1.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.12.0 // indirect + github.com/muesli/termenv v0.13.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect @@ -78,7 +79,7 @@ require ( github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect - github.com/yuin/goldmark v1.4.13 // indirect + github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/sys v0.8.0 // indirect diff --git a/go.sum b/go.sum index 14af85480..087dfa690 100644 --- a/go.sum +++ b/go.sum @@ -8,14 +8,16 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM= -github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94= +github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= +github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= @@ -23,8 +25,8 @@ github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8= github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -github.com/cli/go-gh/v2 v2.1.0 h1:JzYEJyv3VOoU+O9prmoAb3q4JvCwx8dGgLjJF992+18= -github.com/cli/go-gh/v2 v2.1.0/go.mod h1:DwqlWB1TCBcKmDt/9/81EnlbGG7elLoevF4ypt5BnzE= +github.com/cli/go-gh/v2 v2.3.0 h1:FAQAP4PaWSAJf4VSxFEIYDQ1oBIs+bKB4GXQAiRr2sQ= +github.com/cli/go-gh/v2 v2.3.0/go.mod h1:6WBUuf7LUVAc+eXYYX/nYTYURRc6M03K9cJNwBKvwT0= github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA= github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= @@ -113,16 +115,14 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE= -github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y= -github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= @@ -165,9 +165,9 @@ github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:Buzhfgf github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= +github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= @@ -176,10 +176,10 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= @@ -198,7 +198,6 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index df6819e1f..44579b189 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -2,132 +2,74 @@ package prompter import ( "fmt" - "io" "strings" "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/internal/ghinstance" - "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/surveyext" + ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" ) //go:generate moq -rm -out prompter_mock.go . Prompter type Prompter interface { + // generic prompts from go-gh Select(string, string, []string) (int, error) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) Input(string, string) (string, error) - InputHostname() (string, error) Password(string) (string, error) - AuthToken() (string, error) Confirm(string, bool) (bool, error) + + // gh specific prompts + AuthToken() (string, error) ConfirmDeletion(string) error + InputHostname() (string, error) MarkdownEditor(string, string, bool) (string, error) } -type fileWriter interface { - io.Writer - Fd() uintptr -} - -type fileReader interface { - io.Reader - Fd() uintptr -} - -func New(editorCmd string, stdin fileReader, stdout fileWriter, stderr io.Writer) Prompter { +func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { return &surveyPrompter{ - editorCmd: editorCmd, + prompter: ghPrompter.New(stdin, stdout, stderr), stdin: stdin, stdout: stdout, stderr: stderr, + editorCmd: editorCmd, } } type surveyPrompter struct { + prompter *ghPrompter.Prompter + stdin ghPrompter.FileReader + stdout ghPrompter.FileWriter + stderr ghPrompter.FileWriter editorCmd string - stdin fileReader - stdout fileWriter - stderr io.Writer } -// LatinMatchingFilter returns whether the value matches the input filter. -// The strings are compared normalized in case. -// The filter's diactritics are kept as-is, but the value's are normalized, -// so that a missing diactritic in the filter still returns a result. -func LatinMatchingFilter(filter, value string, index int) bool { - filter = strings.ToLower(filter) - value = strings.ToLower(value) - - // include this option if it matches. - return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter) +func (p *surveyPrompter) Select(prompt, defaultValue string, options []string) (int, error) { + return p.prompter.Select(prompt, defaultValue, options) } -func (p *surveyPrompter) Select(message, defaultValue string, options []string) (result int, err error) { - q := &survey.Select{ - Message: message, - Options: options, - PageSize: 20, - Filter: LatinMatchingFilter, - } - - if defaultValue != "" { - // in some situations, defaultValue ends up not being a valid option; do - // not set default in that case as it will make survey panic - for _, o := range options { - if o == defaultValue { - q.Default = defaultValue - break - } - } - } - - err = p.ask(q, &result) - - return +func (p *surveyPrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + return p.prompter.MultiSelect(prompt, defaultValues, options) } -func (p *surveyPrompter) MultiSelect(message string, defaultValues, options []string) (result []int, err error) { - q := &survey.MultiSelect{ - Message: message, - Options: options, - PageSize: 20, - Filter: LatinMatchingFilter, - } - - if len(defaultValues) > 0 { - // TODO I don't actually know that this is needed, just being extra cautious - validatedDefault := []string{} - for _, x := range defaultValues { - for _, y := range options { - if x == y { - validatedDefault = append(validatedDefault, x) - } - } - } - q.Default = validatedDefault - } - - err = p.ask(q, &result) - - return +func (p *surveyPrompter) Input(prompt, defaultValue string) (string, error) { + return p.prompter.Input(prompt, defaultValue) } -func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { - opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) - err := survey.AskOne(q, response, opts...) - if err == nil { - return nil - } - return fmt.Errorf("could not prompt: %w", err) +func (p *surveyPrompter) Password(prompt string) (string, error) { + return p.prompter.Password(prompt) } -func (p *surveyPrompter) Input(prompt, defaultValue string) (result string, err error) { - err = p.ask(&survey.Input{ - Message: prompt, - Default: defaultValue, - }, &result) +func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { + return p.prompter.Confirm(prompt, defaultValue) +} - return +func (p *surveyPrompter) AuthToken() (string, error) { + var result string + err := p.ask(&survey.Password{ + Message: "Paste your authentication token:", + }, &result, survey.WithValidator(survey.Required)) + return result, err } func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error { @@ -146,54 +88,38 @@ func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error { })) } -func (p *surveyPrompter) InputHostname() (result string, err error) { - err = p.ask( +func (p *surveyPrompter) InputHostname() (string, error) { + var result string + err := p.ask( &survey.Input{ Message: "GHE hostname:", }, &result, survey.WithValidator(func(v interface{}) error { return ghinstance.HostnameValidator(v.(string)) })) - - return + return result, err } -func (p *surveyPrompter) Password(prompt string) (result string, err error) { - err = p.ask(&survey.Password{ - Message: prompt, - }, &result) - - return -} - -func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (result bool, err error) { - err = p.ask(&survey.Confirm{ - Message: prompt, - Default: defaultValue, - }, &result) - - return -} -func (p *surveyPrompter) MarkdownEditor(message, defaultValue string, blankAllowed bool) (result string, err error) { - - err = p.ask(&surveyext.GhEditor{ +func (p *surveyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { + var result string + err := p.ask(&surveyext.GhEditor{ BlankAllowed: blankAllowed, EditorCommand: p.editorCmd, Editor: &survey.Editor{ - Message: message, + Message: prompt, Default: defaultValue, FileName: "*.md", HideDefault: true, AppendDefault: true, }, }, &result) - - return + return result, err } -func (p *surveyPrompter) AuthToken() (result string, err error) { - err = p.ask(&survey.Password{ - Message: "Paste your authentication token:", - }, &result, survey.WithValidator(survey.Required)) - - return +func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { + opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) + err := survey.AskOne(q, response, opts...) + if err == nil { + return nil + } + return fmt.Errorf("could not prompt: %w", err) } diff --git a/internal/prompter/prompter_test.go b/internal/prompter/prompter_test.go deleted file mode 100644 index 98d78786b..000000000 --- a/internal/prompter/prompter_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package prompter - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFilterDiacritics(t *testing.T) { - tests := []struct { - name string - filter string - value string - want bool - }{ - { - name: "exact match no diacritics", - filter: "Mikelis", - value: "Mikelis", - want: true, - }, - { - name: "exact match no diacritics", - filter: "Mikelis", - value: "Mikelis", - want: true, - }, - { - name: "exact match diacritics", - filter: "Miķelis", - value: "Miķelis", - want: true, - }, - { - name: "partial match diacritics", - filter: "Miķe", - value: "Miķelis", - want: true, - }, - { - name: "exact match diacritics in value", - filter: "Mikelis", - value: "Miķelis", - want: true, - }, - { - name: "partial match diacritics in filter", - filter: "Miķe", - value: "Miķelis", - want: true, - }, - { - name: "no match when removing diacritics in filter", - filter: "Mielis", - value: "Mikelis", - want: false, - }, - { - name: "no match when removing diacritics in value", - filter: "Mikelis", - value: "Mielis", - want: false, - }, - { - name: "no match diacritics in filter", - filter: "Miķelis", - value: "Mikelis", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, LatinMatchingFilter(tt.filter, tt.value, 0), tt.want) - }) - } - -} diff --git a/internal/prompter/test.go b/internal/prompter/test.go index 0dea18e5a..af55c084d 100644 --- a/internal/prompter/test.go +++ b/internal/prompter/test.go @@ -5,10 +5,132 @@ import ( "strings" "testing" + ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" "github.com/stretchr/testify/assert" ) -// Test helpers +func NewMockPrompter(t *testing.T) *MockPrompter { + m := &MockPrompter{ + t: t, + PrompterMock: *ghPrompter.NewMock(t), + authTokenStubs: []authTokenStub{}, + confirmDeletionStubs: []confirmDeletionStub{}, + inputHostnameStubs: []inputHostnameStub{}, + markdownEditorStubs: []markdownEditorStub{}, + } + t.Cleanup(m.Verify) + return m +} + +type MockPrompter struct { + t *testing.T + ghPrompter.PrompterMock + authTokenStubs []authTokenStub + confirmDeletionStubs []confirmDeletionStub + inputHostnameStubs []inputHostnameStub + markdownEditorStubs []markdownEditorStub +} + +type authTokenStub struct { + fn func() (string, error) +} + +type confirmDeletionStub struct { + prompt string + fn func(string) error +} + +type inputHostnameStub struct { + fn func() (string, error) +} + +type markdownEditorStub struct { + prompt string + fn func(string, string, bool) (string, error) +} + +func (m *MockPrompter) AuthToken() (string, error) { + var s authTokenStub + if len(m.authTokenStubs) == 0 { + return "", NoSuchPromptErr("AuthToken") + } + s = m.authTokenStubs[0] + m.authTokenStubs = m.authTokenStubs[1:len(m.authTokenStubs)] + return s.fn() +} + +func (m *MockPrompter) ConfirmDeletion(prompt string) error { + var s confirmDeletionStub + if len(m.confirmDeletionStubs) == 0 { + return NoSuchPromptErr("ConfirmDeletion") + } + s = m.confirmDeletionStubs[0] + m.confirmDeletionStubs = m.confirmDeletionStubs[1:len(m.confirmDeletionStubs)] + return s.fn(prompt) +} + +func (m *MockPrompter) InputHostname() (string, error) { + var s inputHostnameStub + if len(m.inputHostnameStubs) == 0 { + return "", NoSuchPromptErr("InputHostname") + } + s = m.inputHostnameStubs[0] + m.inputHostnameStubs = m.inputHostnameStubs[1:len(m.inputHostnameStubs)] + return s.fn() +} + +func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { + var s markdownEditorStub + if len(m.markdownEditorStubs) == 0 { + return "", NoSuchPromptErr(prompt) + } + s = m.markdownEditorStubs[0] + m.markdownEditorStubs = m.markdownEditorStubs[1:len(m.markdownEditorStubs)] + if s.prompt != prompt { + return "", NoSuchPromptErr(prompt) + } + return s.fn(prompt, defaultValue, blankAllowed) +} + +func (m *MockPrompter) RegisterAuthToken(stub func() (string, error)) { + m.authTokenStubs = append(m.authTokenStubs, authTokenStub{fn: stub}) +} + +func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) { + m.confirmDeletionStubs = append(m.confirmDeletionStubs, confirmDeletionStub{prompt: prompt, fn: stub}) +} + +func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) { + m.inputHostnameStubs = append(m.inputHostnameStubs, inputHostnameStub{fn: stub}) +} + +func (m *MockPrompter) RegisterMarkdownEditor(prompt string, stub func(string, string, bool) (string, error)) { + m.markdownEditorStubs = append(m.markdownEditorStubs, markdownEditorStub{prompt: prompt, fn: stub}) +} + +func (m *MockPrompter) Verify() { + errs := []string{} + if len(m.authTokenStubs) > 0 { + errs = append(errs, "AuthToken") + } + if len(m.confirmDeletionStubs) > 0 { + errs = append(errs, "ConfirmDeletion") + } + if len(m.inputHostnameStubs) > 0 { + errs = append(errs, "inputHostname") + } + if len(m.markdownEditorStubs) > 0 { + errs = append(errs, "markdownEditorStubs") + } + if len(errs) > 0 { + m.t.Helper() + m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ",")) + } +} + +func AssertOptions(t *testing.T, expected, actual []string) { + assert.Equal(t, expected, actual) +} func IndexFor(options []string, answer string) (int, error) { for ix, a := range options { @@ -19,267 +141,10 @@ func IndexFor(options []string, answer string) (int, error) { return -1, NoSuchAnswerErr(answer) } -func AssertOptions(t *testing.T, expected, actual []string) { - assert.Equal(t, expected, actual) +func NoSuchPromptErr(prompt string) error { + return fmt.Errorf("no such prompt '%s'", prompt) } func NoSuchAnswerErr(answer string) error { return fmt.Errorf("no such answer '%s'", answer) } - -func NoSuchPromptErr(prompt string) error { - return fmt.Errorf("no such prompt '%s'", prompt) -} - -type SelectStub struct { - Prompt string - ExpectedOpts []string - Fn func(string, string, []string) (int, error) -} - -type InputStub struct { - Prompt string - Fn func(string, string) (string, error) -} - -type ConfirmStub struct { - Prompt string - Fn func(string, bool) (bool, error) -} - -type MultiSelectStub struct { - Prompt string - ExpectedOpts []string - Fn func(string, []string, []string) ([]int, error) -} - -type InputHostnameStub struct { - Fn func() (string, error) -} - -type PasswordStub struct { - Prompt string - Fn func(string) (string, error) -} - -type AuthTokenStub struct { - Fn func() (string, error) -} - -type ConfirmDeletionStub struct { - Prompt string - Fn func(string) error -} - -type MockPrompter struct { - PrompterMock - t *testing.T - SelectStubs []SelectStub - InputStubs []InputStub - ConfirmStubs []ConfirmStub - MultiSelectStubs []MultiSelectStub - InputHostnameStubs []InputHostnameStub - PasswordStubs []PasswordStub - AuthTokenStubs []AuthTokenStub - ConfirmDeletionStubs []ConfirmDeletionStub -} - -func NewMockPrompter(t *testing.T) *MockPrompter { - m := &MockPrompter{ - t: t, - SelectStubs: []SelectStub{}, - InputStubs: []InputStub{}, - ConfirmStubs: []ConfirmStub{}, - MultiSelectStubs: []MultiSelectStub{}, - InputHostnameStubs: []InputHostnameStub{}, - PasswordStubs: []PasswordStub{}, - AuthTokenStubs: []AuthTokenStub{}, - ConfirmDeletionStubs: []ConfirmDeletionStub{}, - } - - t.Cleanup(m.Verify) - - m.SelectFunc = func(p, d string, opts []string) (int, error) { - var s SelectStub - - if len(m.SelectStubs) > 0 { - s = m.SelectStubs[0] - m.SelectStubs = m.SelectStubs[1:len(m.SelectStubs)] - } else { - return -1, NoSuchPromptErr(p) - } - - if s.Prompt != p { - return -1, NoSuchPromptErr(p) - } - - AssertOptions(m.t, s.ExpectedOpts, opts) - - return s.Fn(p, d, opts) - } - - m.MultiSelectFunc = func(p string, d, opts []string) ([]int, error) { - var s MultiSelectStub - if len(m.MultiSelectStubs) > 0 { - s = m.MultiSelectStubs[0] - m.MultiSelectStubs = m.MultiSelectStubs[1:len(m.MultiSelectStubs)] - } else { - return []int{}, NoSuchPromptErr(p) - } - - if s.Prompt != p { - return []int{}, NoSuchPromptErr(p) - } - - AssertOptions(m.t, s.ExpectedOpts, opts) - - return s.Fn(p, d, opts) - } - - m.InputFunc = func(p, d string) (string, error) { - var s InputStub - - if len(m.InputStubs) > 0 { - s = m.InputStubs[0] - m.InputStubs = m.InputStubs[1:len(m.InputStubs)] - } else { - return "", NoSuchPromptErr(p) - } - - if s.Prompt != p { - return "", NoSuchPromptErr(p) - } - - return s.Fn(p, d) - } - - m.ConfirmFunc = func(p string, d bool) (bool, error) { - var s ConfirmStub - - if len(m.ConfirmStubs) > 0 { - s = m.ConfirmStubs[0] - m.ConfirmStubs = m.ConfirmStubs[1:len(m.ConfirmStubs)] - } else { - return false, NoSuchPromptErr(p) - } - - if s.Prompt != p { - return false, NoSuchPromptErr(p) - } - - return s.Fn(p, d) - } - - m.InputHostnameFunc = func() (string, error) { - var s InputHostnameStub - - if len(m.InputHostnameStubs) > 0 { - s = m.InputHostnameStubs[0] - m.InputHostnameStubs = m.InputHostnameStubs[1:len(m.InputHostnameStubs)] - } else { - return "", NoSuchPromptErr("InputHostname") - } - - return s.Fn() - } - - m.PasswordFunc = func(p string) (string, error) { - var s PasswordStub - - if len(m.PasswordStubs) > 0 { - s = m.PasswordStubs[0] - m.PasswordStubs = m.PasswordStubs[1:len(m.PasswordStubs)] - } else { - return "", NoSuchPromptErr(p) - } - - if s.Prompt != p { - return "", NoSuchPromptErr(p) - } - - return s.Fn(p) - } - - m.AuthTokenFunc = func() (string, error) { - var s AuthTokenStub - - if len(m.AuthTokenStubs) > 0 { - s = m.AuthTokenStubs[0] - m.AuthTokenStubs = m.AuthTokenStubs[1:len(m.AuthTokenStubs)] - } else { - return "", NoSuchPromptErr("AuthToken") - } - - return s.Fn() - } - - m.ConfirmDeletionFunc = func(p string) error { - var s ConfirmDeletionStub - - if len(m.ConfirmDeletionStubs) > 0 { - s = m.ConfirmDeletionStubs[0] - m.ConfirmDeletionStubs = m.ConfirmDeletionStubs[1:len(m.ConfirmDeletionStubs)] - } else { - return NoSuchPromptErr("ConfirmDeletion") - } - - return s.Fn(p) - } - - // TODO MarkdownEditor(string, string, bool) (string, error) - - return m -} - -func (m *MockPrompter) RegisterSelect(prompt string, opts []string, stub func(_, _ string, _ []string) (int, error)) { - m.SelectStubs = append(m.SelectStubs, SelectStub{ - Prompt: prompt, - ExpectedOpts: opts, - Fn: stub}) -} - -func (m *MockPrompter) RegisterMultiSelect(prompt string, d, opts []string, stub func(_ string, _, _ []string) ([]int, error)) { - m.MultiSelectStubs = append(m.MultiSelectStubs, MultiSelectStub{ - Prompt: prompt, - ExpectedOpts: opts, - Fn: stub}) -} - -func (m *MockPrompter) RegisterInput(prompt string, stub func(_, _ string) (string, error)) { - m.InputStubs = append(m.InputStubs, InputStub{Prompt: prompt, Fn: stub}) -} - -func (m *MockPrompter) RegisterConfirm(prompt string, stub func(_ string, _ bool) (bool, error)) { - m.ConfirmStubs = append(m.ConfirmStubs, ConfirmStub{Prompt: prompt, Fn: stub}) -} - -func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) { - m.InputHostnameStubs = append(m.InputHostnameStubs, InputHostnameStub{Fn: stub}) -} - -func (m *MockPrompter) RegisterPassword(prompt string, stub func(string) (string, error)) { - m.PasswordStubs = append(m.PasswordStubs, PasswordStub{Prompt: prompt, Fn: stub}) -} - -func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) { - m.ConfirmDeletionStubs = append(m.ConfirmDeletionStubs, ConfirmDeletionStub{Prompt: prompt, Fn: stub}) -} - -func (m *MockPrompter) Verify() { - errs := []string{} - if len(m.SelectStubs) > 0 { - errs = append(errs, "Select") - } - if len(m.InputStubs) > 0 { - errs = append(errs, "Input") - } - if len(m.ConfirmStubs) > 0 { - errs = append(errs, "Confirm") - } - // TODO other prompt types - - if len(errs) > 0 { - m.t.Helper() - m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ",")) - } -} diff --git a/internal/text/text.go b/internal/text/text.go index 034d2f6e6..6eb541584 100644 --- a/internal/text/text.go +++ b/internal/text/text.go @@ -6,14 +6,10 @@ import ( "regexp" "strings" "time" - "unicode" "github.com/cli/go-gh/v2/pkg/text" "golang.org/x/text/cases" "golang.org/x/text/language" - "golang.org/x/text/runes" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" ) var whitespaceRE = regexp.MustCompile(`\s+`) @@ -79,16 +75,5 @@ func DisplayURL(urlStr string) string { // RemoveDiacritics returns the input value without "diacritics", or accent marks func RemoveDiacritics(value string) string { - // Mn = "Mark, nonspacing" unicode character category - removeMnTransfomer := runes.Remove(runes.In(unicode.Mn)) - - // 1/ Decompose the text into characters and diacritical marks, - // 2/ Remove the diacriticals marks - // 3/ Recompose the text - t := transform.Chain(norm.NFD, removeMnTransfomer, norm.NFC) - normalized, _, err := transform.String(t, value) - if err != nil { - return value - } - return normalized + return text.RemoveDiacritics(value) } diff --git a/internal/text/text_test.go b/internal/text/text_test.go index 794d2327b..98f16da5d 100644 --- a/internal/text/text_test.go +++ b/internal/text/text_test.go @@ -54,57 +54,3 @@ func TestFuzzyAgoAbbr(t *testing.T) { assert.Equal(t, expected, fuzzy) } } - -func TestRemoveDiacritics(t *testing.T) { - tests := [][]string{ - // no diacritics - {"e", "e"}, - {"و", "و"}, - {"И", "И"}, - {"ж", "ж"}, - {"私", "私"}, - {"万", "万"}, - - // diacritics test sets - {"à", "a"}, - {"é", "e"}, - {"è", "e"}, - {"ô", "o"}, - {"ᾳ", "α"}, - {"εͅ", "ε"}, - {"ῃ", "η"}, - {"ιͅ", "ι"}, - - {"ؤ", "و"}, - - {"ā", "a"}, - {"č", "c"}, - {"ģ", "g"}, - {"ķ", "k"}, - {"ņ", "n"}, - {"š", "s"}, - {"ž", "z"}, - - {"ŵ", "w"}, - {"ŷ", "y"}, - {"ä", "a"}, - {"ÿ", "y"}, - {"á", "a"}, - {"ẁ", "w"}, - {"ỳ", "y"}, - {"ō", "o"}, - - // full words - {"Miķelis", "Mikelis"}, - {"François", "Francois"}, - {"žluťoučký", "zlutoucky"}, - {"învățătorița", "invatatorita"}, - {"Kękę przy łóżku", "Keke przy łozku"}, - } - - for _, tt := range tests { - t.Run(RemoveDiacritics(tt[0]), func(t *testing.T) { - assert.Equal(t, tt[1], RemoveDiacritics(tt[0])) - }) - } -} diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 2b41288d2..be057c494 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -51,7 +51,7 @@ type IOStreams struct { In fileReader Out fileWriter - ErrOut io.Writer + ErrOut fileWriter terminalTheme string @@ -396,19 +396,30 @@ func System() *IOStreams { var stdout fileWriter = os.Stdout // On Windows with no virtual terminal processing support, translate ANSI escape - // sequences to console syscalls + // sequences to console syscalls. if colorableStdout := colorable.NewColorable(os.Stdout); colorableStdout != os.Stdout { - // ensure that the file descriptor of the original stdout is preserved + // Ensure that the file descriptor of the original stdout is preserved. stdout = &fdWriter{ fd: os.Stdout.Fd(), Writer: colorableStdout, } } + var stderr fileWriter = os.Stderr + // On Windows with no virtual terminal processing support, translate ANSI escape + // sequences to console syscalls. + if colorableStderr := colorable.NewColorable(os.Stderr); colorableStderr != os.Stderr { + // Ensure that the file descriptor of the original stderr is preserved. + stderr = &fdWriter{ + fd: os.Stderr.Fd(), + Writer: colorableStderr, + } + } + io := &IOStreams{ In: os.Stdin, Out: stdout, - ErrOut: colorable.NewColorable(os.Stderr), + ErrOut: stderr, pagerCommand: os.Getenv("PAGER"), term: &terminal, } @@ -463,7 +474,7 @@ func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) { ReadCloser: io.NopCloser(in), }, Out: &fdWriter{fd: 1, Writer: out}, - ErrOut: errOut, + ErrOut: &fdWriter{fd: 2, Writer: errOut}, term: &fakeTerm{}, } io.SetStdinTTY(false)