diff --git a/cmd/edit.go b/cmd/edit.go index 3b5ef70..dc66edb 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -29,6 +29,8 @@ func edit(cmd *cobra.Command, args []string) (err error) { options = append(options, fmt.Sprintf("--query %s", shellescape.Quote(flag.Query))) } + // If we have multiple snippet directories, we need to find the right + // snippet file to edit - so we need to prompt the user to select a snippet first if len(config.Conf.General.SnippetDirs) > 0 { snippetFile, err = selectFile(options, flag.FilterTag) if err != nil { @@ -41,21 +43,19 @@ func edit(cmd *cobra.Command, args []string) (err error) { } // file content before editing - before := fileContent(snippetFile) - + contentBefore := fileContent(snippetFile) err = editFile(editor, snippetFile, 0) if err != nil { return } + contentAfter := fileContent(snippetFile) - // file content after editing - after := fileContent(snippetFile) - - // return if same file content - if before == after { + // no need to try to sync if same file content + if contentBefore == contentAfter { return nil } + // sync snippet file if config.Conf.Gist.AutoSync { return petSync.AutoSync(snippetFile) } diff --git a/cmd/list.go b/cmd/list.go index 7b24640..c55f077 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -25,7 +25,7 @@ var listCmd = &cobra.Command{ func list(cmd *cobra.Command, args []string) error { var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(true); err != nil { return err } diff --git a/cmd/new.go b/cmd/new.go index 7fac3dc..e9857a1 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -142,8 +142,8 @@ func scanMultiLine(prompt string, secondMessage string, out io.Writer, in io.Rea return "", errors.New("canceled") } -// createAndEditSnippet creates and saves a given snippet, then opens the -// configured editor to edit the snippet file at startLine. +// createAndEditSnippet creates and saves a given snippet to the main snippet file +// then opens the configured editor to edit the snippet file at startLine. func createAndEditSnippet(newSnippet snippet.SnippetInfo, snippets snippet.Snippets, startLine int) error { snippets.Snippets = append(snippets.Snippets, newSnippet) if err := snippets.Save(); err != nil { @@ -179,14 +179,21 @@ func countSnippetLines() int { return lineCount } +// new creates a new snippet and saves it to the main snippet file +// then syncs the snippet file if configured to do so. func new(cmd *cobra.Command, args []string) (err error) { + return _new(os.Stdin, os.Stdout, args) +} + +func _new(in io.ReadCloser, out io.Writer, args []string) (err error) { var filename string = "" var command string var description string var tags []string + // Load snippets from the main file only var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(false); err != nil { return err } @@ -200,7 +207,7 @@ func new(cmd *cobra.Command, args []string) (err error) { command, err = scanMultiLine( color.YellowString("Command> "), color.YellowString(".......> "), - os.Stdout, os.Stdin, + out, in, ) } else if config.Flag.UseEditor { // Create and save empty snippet @@ -213,20 +220,20 @@ func new(cmd *cobra.Command, args []string) (err error) { return createAndEditSnippet(newSnippet, snippets, lineCount+3) } else { - command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false) + command, err = scan(color.HiYellowString("Command> "), out, in, false) } if err != nil { return err } } - description, err = scan(color.HiGreenString("Description> "), os.Stdout, os.Stdin, false) + description, err = scan(color.HiGreenString("Description> "), out, in, false) if err != nil { return err } if config.Flag.Tag { var t string - if t, err = scan(color.HiCyanString("Tag> "), os.Stdout, os.Stdin, true); err != nil { + if t, err = scan(color.HiCyanString("Tag> "), out, in, true); err != nil { return err } diff --git a/cmd/new_test.go b/cmd/new_test.go index 4959865..15f1f98 100644 --- a/cmd/new_test.go +++ b/cmd/new_test.go @@ -2,8 +2,14 @@ package cmd import ( "bytes" + "os" + "path/filepath" "strings" "testing" + + "github.com/knqyf263/pet/config" + "github.com/knqyf263/pet/snippet" + "github.com/pelletier/go-toml" ) // MockReadCloser is a mock implementation of io.ReadCloser @@ -127,3 +133,121 @@ func TestScanMultiLine_ExitsOnTwoEmptyLines(t *testing.T) { t.Errorf("Expected error %v, but got %v", expectedError, err) } } + +func TestNewSnippetCreationWithSnippetDirectory(t *testing.T) { + // Setup temporary directory for config + tempDir := t.TempDir() + tempSnippetFile := filepath.Join(tempDir, "snippet.toml") + tempSnippetDir1 := filepath.Join(tempDir, "snippets1") + tempSnippetDir2 := filepath.Join(tempDir, "snippets2") + + // Create snippet directories + if err := os.Mkdir(tempSnippetDir1, 0755); err != nil { + t.Fatalf("Failed to create temp snippet directory: %v", err) + } + if err := os.Mkdir(tempSnippetDir2, 0755); err != nil { + t.Fatalf("Failed to create temp snippet directory: %v", err) + } + + // Create dummy snippets in the main snippet file + mainSnippets := snippet.Snippets{ + Snippets: []snippet.SnippetInfo{ + {Description: "main snippet 1", Command: "echo main1"}, + {Description: "main snippet 2", Command: "echo main2"}, + }, + } + saveSnippetsToFile(t, tempSnippetFile, mainSnippets) + + // Create dummy snippets in the snippet directories + dirSnippets1 := snippet.Snippets{ + Snippets: []snippet.SnippetInfo{ + {Description: "dir1 snippet 1", Command: "echo dir1-1"}, + }, + } + dirSnippets2 := snippet.Snippets{ + Snippets: []snippet.SnippetInfo{ + {Description: "dir2 snippet 1", Command: "echo dir2-1"}, + }, + } + saveSnippetsToFile(t, filepath.Join(tempSnippetDir1, "snippets1.toml"), dirSnippets1) + saveSnippetsToFile(t, filepath.Join(tempSnippetDir2, "snippets2.toml"), dirSnippets2) + + // Mock configuration + config.Conf.General.SnippetFile = tempSnippetFile + config.Conf.General.SnippetDirs = []string{tempSnippetDir1, tempSnippetDir2} + + // Simulate creating a new snippet + args := []string{"echo new command"} + + // Create a buffer for output + var outputBuffer bytes.Buffer + // Create a mock ReadCloser for input + inputReader := &MockReadCloser{strings.NewReader("test\ntest")} + + err := _new(inputReader, &outputBuffer, args) + if err != nil { + t.Fatalf("Failed to create new snippet: %v", err) + } + + // Load the main snippet file and check: + // 1 - if the new snippet is added + // 2 - if the number of snippets is correct (to avoid bugs like overwriting with dir snippets) + var updatedMainSnippets snippet.Snippets + loadSnippetsFromFile(t, tempSnippetFile, &updatedMainSnippets) + + if len(updatedMainSnippets.Snippets) != 3 { + t.Fatalf("Expected 3 snippets in main snippet file, got %d", len(updatedMainSnippets.Snippets)) + } + + newSnippet := updatedMainSnippets.Snippets[2] + if newSnippet.Command != "echo new command" { + t.Errorf("Expected new command to be 'echo new command', got '%s'", newSnippet.Command) + } + + // Ensure the snippet files in the directories remain unchanged + var unchangedDirSnippets1, unchangedDirSnippets2 snippet.Snippets + loadSnippetsFromFile(t, filepath.Join(tempSnippetDir1, "snippets1.toml"), &unchangedDirSnippets1) + loadSnippetsFromFile(t, filepath.Join(tempSnippetDir2, "snippets2.toml"), &unchangedDirSnippets2) + + if !compareSnippets(dirSnippets1, unchangedDirSnippets1) { + t.Errorf("Snippets in directory 1 have changed") + } + if !compareSnippets(dirSnippets2, unchangedDirSnippets2) { + t.Errorf("Snippets in directory 2 have changed") + } +} + +func saveSnippetsToFile(t *testing.T, filename string, snippets snippet.Snippets) { + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Failed to create snippet file: %v", err) + } + defer f.Close() + + if err := toml.NewEncoder(f).Encode(snippets); err != nil { + t.Fatalf("Failed to encode snippets to file: %v", err) + } +} + +func loadSnippetsFromFile(t *testing.T, filename string, snippets *snippet.Snippets) { + f, err := os.ReadFile(filename) + if err != nil { + t.Fatalf("Failed to read snippet file: %v", err) + } + + if err := toml.Unmarshal(f, snippets); err != nil { + t.Fatalf("Failed to unmarshal snippets from file: %v", err) + } +} + +func compareSnippets(a, b snippet.Snippets) bool { + if len(a.Snippets) != len(b.Snippets) { + return false + } + for i := range a.Snippets { + if a.Snippets[i].Description != b.Snippets[i].Description || a.Snippets[i].Command != b.Snippets[i].Command { + return false + } + } + return true +} diff --git a/cmd/search.go b/cmd/search.go index bc63738..149db45 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -6,12 +6,10 @@ import ( "github.com/knqyf263/pet/config" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "gopkg.in/alessio/shellescape.v1" ) -var delimiter string - // searchCmd represents the search command var searchCmd = &cobra.Command{ Use: "search", @@ -33,7 +31,7 @@ func search(cmd *cobra.Command, args []string) (err error) { } fmt.Print(strings.Join(commands, flag.Delimiter)) - if terminal.IsTerminal(1) { + if term.IsTerminal(1) { fmt.Print("\n") } return nil diff --git a/cmd/util.go b/cmd/util.go index a07c058..7f5b522 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -23,10 +23,11 @@ func editFile(command, file string, startingLine int) error { func filter(options []string, tag string) (commands []string, err error) { var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(true); err != nil { return commands, fmt.Errorf("load snippet failed: %v", err) } + // Filter the snippets by specified tag if any if 0 < len(tag) { var filteredSnippets snippet.Snippets for _, snippet := range snippets.Snippets { @@ -103,12 +104,16 @@ func filter(options []string, tag string) (commands []string, err error) { return commands, nil } +// selectFile returns a snippet file path from the list of snippets +// options are simply the list of arguments to pass to the select command (ex. --query for fzf) +// tag is used to filter the list of snippets by the tag field in the snippet func selectFile(options []string, tag string) (snippetFile string, err error) { var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(true); err != nil { return snippetFile, fmt.Errorf("load snippet failed: %v", err) } + // Filter the snippets by specified tag if any if 0 < len(tag) { var filteredSnippets snippet.Snippets for _, snippet := range snippets.Snippets { @@ -121,6 +126,7 @@ func selectFile(options []string, tag string) (snippetFile string, err error) { snippets = filteredSnippets } + // Create a map of (desc, command, tags) string to SnippetInfo snippetTexts := map[string]snippet.SnippetInfo{} var text string for _, s := range snippets.Snippets { @@ -140,6 +146,7 @@ func selectFile(options []string, tag string) (snippetFile string, err error) { text += t + "\n" } + // Build the select command with options and run it var buf bytes.Buffer selectCmd := fmt.Sprintf("%s %s", config.Conf.General.SelectCmd, strings.Join(options, " ")) @@ -148,8 +155,8 @@ func selectFile(options []string, tag string) (snippetFile string, err error) { return snippetFile, nil } + // Parse the selected line and return the corresponding snippet file lines := strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") - for _, line := range lines { snippetInfo := snippetTexts[line] snippetFile = fmt.Sprint(snippetInfo.Filename) diff --git a/go.mod b/go.mod index cb20ef1..18be7d5 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,6 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.1 // indirect github.com/xanzy/go-gitlab v0.50.3 - //github.com/xanzy/go-gitlab v0.10.5 - golang.org/x/crypto v0.17.0 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect ) @@ -33,6 +31,7 @@ require ( github.com/awesome-gocui/gocui v1.1.0 github.com/go-test/deep v1.1.0 github.com/pelletier/go-toml v1.9.5 + golang.org/x/term v0.15.0 ) require ( @@ -47,7 +46,6 @@ require ( github.com/rivo/uniseg v0.1.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect google.golang.org/appengine v1.3.0 // indirect diff --git a/go.sum b/go.sum index 7197e53..ad47f9c 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,6 @@ github.com/xanzy/go-gitlab v0.50.3 h1:M7ncgNhCN4jaFNyXxarJhCLa9Qi6fdmCxFFhMTQPZi github.com/xanzy/go-gitlab v0.50.3/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= diff --git a/snippet/snippet.go b/snippet/snippet.go index 32cf2e4..4ed5a84 100644 --- a/snippet/snippet.go +++ b/snippet/snippet.go @@ -24,10 +24,14 @@ type SnippetInfo struct { Output string } -// Load reads toml file. -func (snippets *Snippets) Load() error { +// Loads snippets from the main snippet file and all snippet +// files in snippet directories if present +func (snippets *Snippets) Load(includeDirs bool) error { + // Create a list of snippet files to load snippets from var snippetFiles []string + // Load snippets from the main snippet file + // Raise an error if the file is not found / not configured snippetFile := config.Conf.General.SnippetFile if snippetFile != "" { if _, err := os.Stat(snippetFile); err == nil { @@ -44,14 +48,17 @@ if you only want to provide snippetdirs instead`, } } - for _, dir := range config.Conf.General.SnippetDirs { - if _, err := os.Stat(dir); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("snippet directory not found. %s", dir) + // Optionally load snippets from snippet directories + if includeDirs { + for _, dir := range config.Conf.General.SnippetDirs { + if _, err := os.Stat(dir); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("snippet directory not found. %s", dir) + } + return fmt.Errorf("failed to load snippet directory. %v", err) } - return fmt.Errorf("failed to load snippet directory. %v", err) + snippetFiles = append(snippetFiles, getFiles(dir)...) } - snippetFiles = append(snippetFiles, getFiles(dir)...) } // Read files and load snippets @@ -81,6 +88,7 @@ if you only want to provide snippetdirs instead`, func (snippets *Snippets) Save() error { var snippetFile string var newSnippets Snippets + for _, snippet := range snippets.Snippets { if snippet.Filename == "" { snippetFile = config.Conf.General.SnippetDirs[0] + fmt.Sprintf("%s.toml", strings.ToLower(sanitize.BaseName(snippet.Description))) diff --git a/sync/sync.go b/sync/sync.go index 3c5a428..6c28344 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -76,9 +76,11 @@ func NewSyncClient() (Client, error) { return client, nil } +// upload uploads snippets from the main snippet file +// to the remote repository - directories are ignored func upload(client Client) (err error) { var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(false); err != nil { return errors.Wrap(err, "Failed to load the local snippets") } @@ -95,11 +97,13 @@ func upload(client Client) (err error) { return nil } +// download downloads snippets from the remote repository +// and saves them to the main snippet file - directories ignored func download(content string) error { snippetFile := config.Conf.General.SnippetFile var snippets snippet.Snippets - if err := snippets.Load(); err != nil { + if err := snippets.Load(false); err != nil { return err } body, err := snippets.ToString()