Skip to content

Commit 89e9f75

Browse files
authored
Merge pull request #301 from knqyf263/multiline-snippet-rebased
Multiline snippet rebased
2 parents b6df04b + adae55d commit 89e9f75

File tree

10 files changed

+228
-42
lines changed

10 files changed

+228
-42
lines changed

cmd/configure.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var configureCmd = &cobra.Command{
1515

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

2121
func init() {

cmd/edit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func edit(cmd *cobra.Command, args []string) (err error) {
2323
// file content before editing
2424
before := fileContent(snippetFile)
2525

26-
err = editFile(editor, snippetFile)
26+
err = editFile(editor, snippetFile, 0)
2727
if err != nil {
2828
return
2929
}

cmd/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func list(cmd *cobra.Command, args []string) error {
3737
for _, snippet := range snippets.Snippets {
3838
if config.Flag.OneLine {
3939
description := runewidth.FillRight(runewidth.Truncate(snippet.Description, col, "..."), col)
40-
command := runewidth.Truncate(snippet.Command, 100-4-col, "...")
40+
command := snippet.Command
4141
// make sure multiline command printed as oneline
4242
command = strings.Replace(command, "\n", "\\n", -1)
4343
fmt.Fprintf(color.Output, "%s : %s\n",

cmd/new.go

Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"path/filepath"
9+
"runtime"
810
"strings"
911

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

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

3840
l, err := readline.NewEx(&readline.Config{
39-
Stdout: out,
40-
Stdin: in,
41-
Prompt: message,
42-
HistoryFile: tempFile,
43-
InterruptPrompt: "^C",
44-
EOFPrompt: "exit",
45-
41+
Stdout: out,
42+
Stdin: in,
43+
Prompt: prompt,
44+
HistoryFile: tempFile,
45+
InterruptPrompt: "^C",
46+
EOFPrompt: "exit",
47+
VimMode: false,
4648
HistorySearchFold: true,
4749
})
4850

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

80+
// States of scanMultiLine state machine
81+
const (
82+
start = iota
83+
lastLineNotEmpty
84+
lastLineEmpty
85+
)
86+
87+
func scanMultiLine(prompt string, secondMessage string, out io.Writer, in io.ReadCloser) (string, error) {
88+
tempFile := "/tmp/pet.tmp"
89+
if runtime.GOOS == "windows" {
90+
tempDir := os.Getenv("TEMP")
91+
tempFile = filepath.Join(tempDir, "pet.tmp")
92+
}
93+
l, err := readline.NewEx(&readline.Config{
94+
Stdout: out,
95+
Stdin: in,
96+
Prompt: prompt,
97+
HistoryFile: tempFile,
98+
InterruptPrompt: "^C",
99+
EOFPrompt: "exit",
100+
VimMode: false,
101+
HistorySearchFold: true,
102+
})
103+
if err != nil {
104+
return "", err
105+
}
106+
defer l.Close()
107+
108+
state := start
109+
multiline := ""
110+
for {
111+
line, err := l.Readline()
112+
if err == readline.ErrInterrupt {
113+
if len(line) == 0 {
114+
break
115+
} else {
116+
continue
117+
}
118+
} else if err == io.EOF {
119+
break
120+
}
121+
switch state {
122+
case start:
123+
if line == "" {
124+
continue
125+
}
126+
multiline += line
127+
state = lastLineNotEmpty
128+
l.SetPrompt(secondMessage)
129+
case lastLineNotEmpty:
130+
if line == "" {
131+
state = lastLineEmpty
132+
continue
133+
}
134+
multiline += "\n" + line
135+
case lastLineEmpty:
136+
if line == "" {
137+
return multiline, nil
138+
}
139+
multiline += "\n" + line
140+
state = lastLineNotEmpty
141+
}
142+
}
143+
return "", errors.New("canceled")
144+
}
145+
146+
// createAndEditSnippet creates and saves a given snippet, then opens the
147+
// configured editor to edit the snippet file at startLine.
148+
func createAndEditSnippet(newSnippet snippet.SnippetInfo, snippets snippet.Snippets, startLine int) error {
149+
snippets.Snippets = append(snippets.Snippets, newSnippet)
150+
if err := snippets.Save(); err != nil {
151+
return err
152+
}
153+
154+
// Open snippet for editing
155+
snippetFile := config.Conf.General.SnippetFile
156+
editor := config.Conf.General.Editor
157+
err := editFile(editor, snippetFile, startLine)
158+
if err != nil {
159+
return err
160+
}
161+
162+
if config.Conf.Gist.AutoSync {
163+
return petSync.AutoSync(snippetFile)
164+
}
165+
166+
return nil
167+
}
168+
169+
func countSnippetLines() int {
170+
// Count lines in snippet file
171+
f, err := os.Open(config.Conf.General.SnippetFile)
172+
if err != nil {
173+
panic("Error reading snippet file")
174+
}
175+
lineCount, err := CountLines(f)
176+
if err != nil {
177+
panic("Error counting lines in snippet file")
178+
}
179+
180+
return lineCount
181+
}
182+
78183
func new(cmd *cobra.Command, args []string) (err error) {
79184
var command string
80185
var description string
@@ -85,11 +190,31 @@ func new(cmd *cobra.Command, args []string) (err error) {
85190
return err
86191
}
87192

193+
lineCount := countSnippetLines()
194+
88195
if len(args) > 0 {
89196
command = strings.Join(args, " ")
90197
fmt.Fprintf(color.Output, "%s %s\n", color.HiYellowString("Command>"), command)
91198
} else {
92-
command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false)
199+
if config.Flag.UseMultiLine {
200+
command, err = scanMultiLine(
201+
color.YellowString("Command> "),
202+
color.YellowString(".......> "),
203+
os.Stdout, os.Stdin,
204+
)
205+
} else if config.Flag.UseEditor {
206+
// Create and save empty snippet
207+
newSnippet := snippet.SnippetInfo{
208+
Description: description,
209+
Command: command,
210+
Tag: tags,
211+
}
212+
213+
return createAndEditSnippet(newSnippet, snippets, lineCount+3)
214+
215+
} else {
216+
command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false)
217+
}
93218
if err != nil {
94219
return err
95220
}
@@ -138,4 +263,8 @@ func init() {
138263
RootCmd.AddCommand(newCmd)
139264
newCmd.Flags().BoolVarP(&config.Flag.Tag, "tag", "t", false,
140265
`Display tag prompt (delimiter: space)`)
266+
newCmd.Flags().BoolVarP(&config.Flag.UseMultiLine, "multiline", "m", false,
267+
`Can enter multiline snippet (Double \n to quit)`)
268+
newCmd.Flags().BoolVarP(&config.Flag.UseEditor, "editor", "e", false,
269+
`Use editor to create snippet`)
141270
}

cmd/new_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ func TestScan(t *testing.T) {
4747
func TestScan_EmptyStringWithAllowEmpty(t *testing.T) {
4848
message := "Enter something: "
4949

50-
input := "\n" // Simulated user input
51-
want := "" // Expected output
52-
expectedError := error(nil)
50+
input := "\n" // Simulated user input
51+
want := "" // Expected output
52+
expectedError := error(nil) // Should not error
5353

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

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

8989
// Check if the input was printed
9090
got := result
91-
9291
// Check if the result matches the expected result
9392
if want != got {
9493
t.Errorf("Expected result %q, but got %q", want, got)
@@ -99,3 +98,32 @@ func TestScan_EmptyStringWithoutAllowEmpty(t *testing.T) {
9998
t.Errorf("Expected error %v, but got %v", expectedError, err)
10099
}
101100
}
101+
102+
func TestScanMultiLine_ExitsOnTwoEmptyLines(t *testing.T) {
103+
prompt := "Enter something: "
104+
secondPrompt := "whatever"
105+
106+
input := "test\nnewline here\nand another;\n\n\n" // Simulated user input
107+
want := "test\nnewline here\nand another;" // Expected output
108+
expectedError := error(nil)
109+
110+
// Create a buffer for output
111+
var outputBuffer bytes.Buffer
112+
// Create a mock ReadCloser for input
113+
inputReader := &MockReadCloser{strings.NewReader(input)}
114+
115+
result, err := scanMultiLine(prompt, secondPrompt, &outputBuffer, inputReader)
116+
117+
// Check if the input was printed
118+
got := result
119+
120+
// Check if the result matches the expected result
121+
if want != got {
122+
t.Errorf("Expected result %q, but got %q", want, got)
123+
}
124+
125+
// Check if the error matches the expected error
126+
if err != expectedError {
127+
t.Errorf("Expected error %v, but got %v", expectedError, err)
128+
}
129+
}

cmd/util.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package cmd
33
import (
44
"bytes"
55
"fmt"
6+
"io"
67
"os"
8+
"strconv"
79
"strings"
810

911
"github.com/fatih/color"
@@ -12,15 +14,17 @@ import (
1214
"github.com/knqyf263/pet/snippet"
1315
)
1416

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

2024
func filter(options []string, tag string) (commands []string, err error) {
2125
var snippets snippet.Snippets
2226
if err := snippets.Load(); err != nil {
23-
return commands, fmt.Errorf("Load snippet failed: %v", err)
27+
return commands, fmt.Errorf("load snippet failed: %v", err)
2428
}
2529

2630
if 0 < len(tag) {
@@ -90,3 +94,23 @@ func filter(options []string, tag string) (commands []string, err error) {
9094
}
9195
return commands, nil
9296
}
97+
98+
// CountLines returns the number of lines in a certain buffer
99+
func CountLines(r io.Reader) (int, error) {
100+
buf := make([]byte, 32*1024)
101+
count := 0
102+
lineSep := []byte{'\n'}
103+
104+
for {
105+
c, err := r.Read(buf)
106+
count += bytes.Count(buf[:c], lineSep)
107+
108+
switch {
109+
case err == io.EOF:
110+
return count, nil
111+
112+
case err != nil:
113+
return count, err
114+
}
115+
}
116+
}

config/config.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"path/filepath"
88
"runtime"
99

10-
"github.com/BurntSushi/toml"
10+
"github.com/pelletier/go-toml"
1111
"github.com/pkg/errors"
1212
)
1313

@@ -69,21 +69,24 @@ var Flag FlagConfig
6969

7070
// FlagConfig is a struct of flag
7171
type FlagConfig struct {
72-
Debug bool
73-
Query string
74-
FilterTag string
75-
Command bool
76-
Delimiter string
77-
OneLine bool
78-
Color bool
79-
Tag bool
72+
Debug bool
73+
Query string
74+
FilterTag string
75+
Command bool
76+
Delimiter string
77+
OneLine bool
78+
Color bool
79+
Tag bool
80+
UseMultiLine bool
81+
UseEditor bool
8082
}
8183

8284
// Load loads a config toml
8385
func (cfg *Config) Load(file string) error {
8486
_, err := os.Stat(file)
8587
if err == nil {
86-
_, err := toml.DecodeFile(file, cfg)
88+
f, err := os.ReadFile(file)
89+
err = toml.Unmarshal(f, cfg)
8790
if err != nil {
8891
return err
8992
}

0 commit comments

Comments
 (0)