Skip to content

Commit

Permalink
Merge pull request #301 from knqyf263/multiline-snippet-rebased
Browse files Browse the repository at this point in the history
Multiline snippet rebased
  • Loading branch information
RamiAwar authored May 2, 2024
2 parents b6df04b + adae55d commit 89e9f75
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 42 deletions.
2 changes: 1 addition & 1 deletion cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var configureCmd = &cobra.Command{

func configure(cmd *cobra.Command, args []string) (err error) {
editor := config.Conf.General.Editor
return editFile(editor, configFile)
return editFile(editor, configFile, 0)
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func edit(cmd *cobra.Command, args []string) (err error) {
// file content before editing
before := fileContent(snippetFile)

err = editFile(editor, snippetFile)
err = editFile(editor, snippetFile, 0)
if err != nil {
return
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func list(cmd *cobra.Command, args []string) error {
for _, snippet := range snippets.Snippets {
if config.Flag.OneLine {
description := runewidth.FillRight(runewidth.Truncate(snippet.Description, col, "..."), col)
command := runewidth.Truncate(snippet.Command, 100-4-col, "...")
command := snippet.Command
// make sure multiline command printed as oneline
command = strings.Replace(command, "\n", "\\n", -1)
fmt.Fprintf(color.Output, "%s : %s\n",
Expand Down
147 changes: 138 additions & 9 deletions cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/chzyer/readline"
Expand All @@ -27,7 +29,7 @@ func CanceledError() error {
return errors.New("canceled")
}

func scan(message string, out io.Writer, in io.ReadCloser, allowEmpty bool) (string, error) {
func scan(prompt string, out io.Writer, in io.ReadCloser, allowEmpty bool) (string, error) {
f, err := os.CreateTemp("", "pet-")
if err != nil {
return "", err
Expand All @@ -36,13 +38,13 @@ func scan(message string, out io.Writer, in io.ReadCloser, allowEmpty bool) (str
tempFile := f.Name()

l, err := readline.NewEx(&readline.Config{
Stdout: out,
Stdin: in,
Prompt: message,
HistoryFile: tempFile,
InterruptPrompt: "^C",
EOFPrompt: "exit",

Stdout: out,
Stdin: in,
Prompt: prompt,
HistoryFile: tempFile,
InterruptPrompt: "^C",
EOFPrompt: "exit",
VimMode: false,
HistorySearchFold: true,
})

Expand Down Expand Up @@ -75,6 +77,109 @@ func scan(message string, out io.Writer, in io.ReadCloser, allowEmpty bool) (str
return "", CanceledError()
}

// States of scanMultiLine state machine
const (
start = iota
lastLineNotEmpty
lastLineEmpty
)

func scanMultiLine(prompt string, secondMessage string, out io.Writer, in io.ReadCloser) (string, error) {
tempFile := "/tmp/pet.tmp"
if runtime.GOOS == "windows" {
tempDir := os.Getenv("TEMP")
tempFile = filepath.Join(tempDir, "pet.tmp")
}
l, err := readline.NewEx(&readline.Config{
Stdout: out,
Stdin: in,
Prompt: prompt,
HistoryFile: tempFile,
InterruptPrompt: "^C",
EOFPrompt: "exit",
VimMode: false,
HistorySearchFold: true,
})
if err != nil {
return "", err
}
defer l.Close()

state := start
multiline := ""
for {
line, err := l.Readline()
if err == readline.ErrInterrupt {
if len(line) == 0 {
break
} else {
continue
}
} else if err == io.EOF {
break
}
switch state {
case start:
if line == "" {
continue
}
multiline += line
state = lastLineNotEmpty
l.SetPrompt(secondMessage)
case lastLineNotEmpty:
if line == "" {
state = lastLineEmpty
continue
}
multiline += "\n" + line
case lastLineEmpty:
if line == "" {
return multiline, nil
}
multiline += "\n" + line
state = lastLineNotEmpty
}
}
return "", errors.New("canceled")
}

// createAndEditSnippet creates and saves a given snippet, 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 {
return err
}

// Open snippet for editing
snippetFile := config.Conf.General.SnippetFile
editor := config.Conf.General.Editor
err := editFile(editor, snippetFile, startLine)
if err != nil {
return err
}

if config.Conf.Gist.AutoSync {
return petSync.AutoSync(snippetFile)
}

return nil
}

func countSnippetLines() int {
// Count lines in snippet file
f, err := os.Open(config.Conf.General.SnippetFile)
if err != nil {
panic("Error reading snippet file")
}
lineCount, err := CountLines(f)
if err != nil {
panic("Error counting lines in snippet file")
}

return lineCount
}

func new(cmd *cobra.Command, args []string) (err error) {
var command string
var description string
Expand All @@ -85,11 +190,31 @@ func new(cmd *cobra.Command, args []string) (err error) {
return err
}

lineCount := countSnippetLines()

if len(args) > 0 {
command = strings.Join(args, " ")
fmt.Fprintf(color.Output, "%s %s\n", color.HiYellowString("Command>"), command)
} else {
command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false)
if config.Flag.UseMultiLine {
command, err = scanMultiLine(
color.YellowString("Command> "),
color.YellowString(".......> "),
os.Stdout, os.Stdin,
)
} else if config.Flag.UseEditor {
// Create and save empty snippet
newSnippet := snippet.SnippetInfo{
Description: description,
Command: command,
Tag: tags,
}

return createAndEditSnippet(newSnippet, snippets, lineCount+3)

} else {
command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false)
}
if err != nil {
return err
}
Expand Down Expand Up @@ -138,4 +263,8 @@ func init() {
RootCmd.AddCommand(newCmd)
newCmd.Flags().BoolVarP(&config.Flag.Tag, "tag", "t", false,
`Display tag prompt (delimiter: space)`)
newCmd.Flags().BoolVarP(&config.Flag.UseMultiLine, "multiline", "m", false,
`Can enter multiline snippet (Double \n to quit)`)
newCmd.Flags().BoolVarP(&config.Flag.UseEditor, "editor", "e", false,
`Use editor to create snippet`)
}
38 changes: 33 additions & 5 deletions cmd/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ func TestScan(t *testing.T) {
func TestScan_EmptyStringWithAllowEmpty(t *testing.T) {
message := "Enter something: "

input := "\n" // Simulated user input
want := "" // Expected output
expectedError := error(nil)
input := "\n" // Simulated user input
want := "" // Expected output
expectedError := error(nil) // Should not error

// Create a buffer for output
var outputBuffer bytes.Buffer
Expand All @@ -61,7 +61,7 @@ func TestScan_EmptyStringWithAllowEmpty(t *testing.T) {
// Check if the input was printed
got := result

// Check if the result matches the expected result
// Check if the result is empty
if want != got {
t.Errorf("Expected result %q, but got %q", want, got)
}
Expand All @@ -88,7 +88,6 @@ func TestScan_EmptyStringWithoutAllowEmpty(t *testing.T) {

// Check if the input was printed
got := result

// Check if the result matches the expected result
if want != got {
t.Errorf("Expected result %q, but got %q", want, got)
Expand All @@ -99,3 +98,32 @@ func TestScan_EmptyStringWithoutAllowEmpty(t *testing.T) {
t.Errorf("Expected error %v, but got %v", expectedError, err)
}
}

func TestScanMultiLine_ExitsOnTwoEmptyLines(t *testing.T) {
prompt := "Enter something: "
secondPrompt := "whatever"

input := "test\nnewline here\nand another;\n\n\n" // Simulated user input
want := "test\nnewline here\nand another;" // Expected output
expectedError := error(nil)

// Create a buffer for output
var outputBuffer bytes.Buffer
// Create a mock ReadCloser for input
inputReader := &MockReadCloser{strings.NewReader(input)}

result, err := scanMultiLine(prompt, secondPrompt, &outputBuffer, inputReader)

// Check if the input was printed
got := result

// Check if the result matches the expected result
if want != got {
t.Errorf("Expected result %q, but got %q", want, got)
}

// Check if the error matches the expected error
if err != expectedError {
t.Errorf("Expected error %v, but got %v", expectedError, err)
}
}
30 changes: 27 additions & 3 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package cmd
import (
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"

"github.com/fatih/color"
Expand All @@ -12,15 +14,17 @@ import (
"github.com/knqyf263/pet/snippet"
)

func editFile(command, file string) error {
command += " " + file
func editFile(command, file string, startingLine int) error {
// Note that this works for most unix editors (nano, vi, vim, etc)
// TODO: Remove for other kinds of editors - this is only for UX
command += " +" + strconv.Itoa(startingLine) + " " + file
return run(command, os.Stdin, os.Stdout)
}

func filter(options []string, tag string) (commands []string, err error) {
var snippets snippet.Snippets
if err := snippets.Load(); err != nil {
return commands, fmt.Errorf("Load snippet failed: %v", err)
return commands, fmt.Errorf("load snippet failed: %v", err)
}

if 0 < len(tag) {
Expand Down Expand Up @@ -90,3 +94,23 @@ func filter(options []string, tag string) (commands []string, err error) {
}
return commands, nil
}

// CountLines returns the number of lines in a certain buffer
func CountLines(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
count := 0
lineSep := []byte{'\n'}

for {
c, err := r.Read(buf)
count += bytes.Count(buf[:c], lineSep)

switch {
case err == io.EOF:
return count, nil

case err != nil:
return count, err
}
}
}
23 changes: 13 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"path/filepath"
"runtime"

"github.com/BurntSushi/toml"
"github.com/pelletier/go-toml"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -69,21 +69,24 @@ var Flag FlagConfig

// FlagConfig is a struct of flag
type FlagConfig struct {
Debug bool
Query string
FilterTag string
Command bool
Delimiter string
OneLine bool
Color bool
Tag bool
Debug bool
Query string
FilterTag string
Command bool
Delimiter string
OneLine bool
Color bool
Tag bool
UseMultiLine bool
UseEditor bool
}

// Load loads a config toml
func (cfg *Config) Load(file string) error {
_, err := os.Stat(file)
if err == nil {
_, err := toml.DecodeFile(file, cfg)
f, err := os.ReadFile(file)
err = toml.Unmarshal(f, cfg)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 89e9f75

Please sign in to comment.