doing 2.0.20 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/doing.rdoc +1 -1
  6. data/lib/doing/version.rb +1 -1
  7. data/lib/helpers/fzf/.goreleaser.yml +119 -0
  8. data/lib/helpers/fzf/.rubocop.yml +28 -0
  9. data/lib/helpers/fzf/ADVANCED.md +565 -0
  10. data/lib/helpers/fzf/BUILD.md +49 -0
  11. data/lib/helpers/fzf/CHANGELOG.md +1193 -0
  12. data/lib/helpers/fzf/Dockerfile +11 -0
  13. data/lib/helpers/fzf/LICENSE +21 -0
  14. data/lib/helpers/fzf/Makefile +166 -0
  15. data/lib/helpers/fzf/README-VIM.md +486 -0
  16. data/lib/helpers/fzf/README.md +712 -0
  17. data/lib/helpers/fzf/bin/fzf-tmux +233 -0
  18. data/lib/helpers/fzf/doc/fzf.txt +512 -0
  19. data/lib/helpers/fzf/go.mod +17 -0
  20. data/lib/helpers/fzf/go.sum +31 -0
  21. data/lib/helpers/fzf/install +382 -0
  22. data/lib/helpers/fzf/install.ps1 +65 -0
  23. data/lib/helpers/fzf/main.go +14 -0
  24. data/lib/helpers/fzf/man/man1/fzf-tmux.1 +68 -0
  25. data/lib/helpers/fzf/man/man1/fzf.1 +1001 -0
  26. data/lib/helpers/fzf/plugin/fzf.vim +1048 -0
  27. data/lib/helpers/fzf/shell/completion.bash +381 -0
  28. data/lib/helpers/fzf/shell/completion.zsh +329 -0
  29. data/lib/helpers/fzf/shell/key-bindings.bash +96 -0
  30. data/lib/helpers/fzf/shell/key-bindings.fish +172 -0
  31. data/lib/helpers/fzf/shell/key-bindings.zsh +114 -0
  32. data/lib/helpers/fzf/src/LICENSE +21 -0
  33. data/lib/helpers/fzf/src/algo/algo.go +884 -0
  34. data/lib/helpers/fzf/src/algo/algo_test.go +197 -0
  35. data/lib/helpers/fzf/src/algo/normalize.go +492 -0
  36. data/lib/helpers/fzf/src/ansi.go +409 -0
  37. data/lib/helpers/fzf/src/ansi_test.go +427 -0
  38. data/lib/helpers/fzf/src/cache.go +81 -0
  39. data/lib/helpers/fzf/src/cache_test.go +39 -0
  40. data/lib/helpers/fzf/src/chunklist.go +89 -0
  41. data/lib/helpers/fzf/src/chunklist_test.go +80 -0
  42. data/lib/helpers/fzf/src/constants.go +85 -0
  43. data/lib/helpers/fzf/src/core.go +351 -0
  44. data/lib/helpers/fzf/src/history.go +96 -0
  45. data/lib/helpers/fzf/src/history_test.go +68 -0
  46. data/lib/helpers/fzf/src/item.go +44 -0
  47. data/lib/helpers/fzf/src/item_test.go +23 -0
  48. data/lib/helpers/fzf/src/matcher.go +235 -0
  49. data/lib/helpers/fzf/src/merger.go +120 -0
  50. data/lib/helpers/fzf/src/merger_test.go +88 -0
  51. data/lib/helpers/fzf/src/options.go +1691 -0
  52. data/lib/helpers/fzf/src/options_test.go +457 -0
  53. data/lib/helpers/fzf/src/pattern.go +425 -0
  54. data/lib/helpers/fzf/src/pattern_test.go +209 -0
  55. data/lib/helpers/fzf/src/protector/protector.go +8 -0
  56. data/lib/helpers/fzf/src/protector/protector_openbsd.go +10 -0
  57. data/lib/helpers/fzf/src/reader.go +201 -0
  58. data/lib/helpers/fzf/src/reader_test.go +63 -0
  59. data/lib/helpers/fzf/src/result.go +243 -0
  60. data/lib/helpers/fzf/src/result_others.go +16 -0
  61. data/lib/helpers/fzf/src/result_test.go +159 -0
  62. data/lib/helpers/fzf/src/result_x86.go +16 -0
  63. data/lib/helpers/fzf/src/terminal.go +2832 -0
  64. data/lib/helpers/fzf/src/terminal_test.go +638 -0
  65. data/lib/helpers/fzf/src/terminal_unix.go +26 -0
  66. data/lib/helpers/fzf/src/terminal_windows.go +45 -0
  67. data/lib/helpers/fzf/src/tokenizer.go +253 -0
  68. data/lib/helpers/fzf/src/tokenizer_test.go +112 -0
  69. data/lib/helpers/fzf/src/tui/dummy.go +46 -0
  70. data/lib/helpers/fzf/src/tui/light.go +987 -0
  71. data/lib/helpers/fzf/src/tui/light_unix.go +110 -0
  72. data/lib/helpers/fzf/src/tui/light_windows.go +145 -0
  73. data/lib/helpers/fzf/src/tui/tcell.go +721 -0
  74. data/lib/helpers/fzf/src/tui/tcell_test.go +392 -0
  75. data/lib/helpers/fzf/src/tui/ttyname_unix.go +47 -0
  76. data/lib/helpers/fzf/src/tui/ttyname_windows.go +14 -0
  77. data/lib/helpers/fzf/src/tui/tui.go +625 -0
  78. data/lib/helpers/fzf/src/tui/tui_test.go +20 -0
  79. data/lib/helpers/fzf/src/util/atomicbool.go +34 -0
  80. data/lib/helpers/fzf/src/util/atomicbool_test.go +17 -0
  81. data/lib/helpers/fzf/src/util/chars.go +198 -0
  82. data/lib/helpers/fzf/src/util/chars_test.go +46 -0
  83. data/lib/helpers/fzf/src/util/eventbox.go +96 -0
  84. data/lib/helpers/fzf/src/util/eventbox_test.go +61 -0
  85. data/lib/helpers/fzf/src/util/slab.go +12 -0
  86. data/lib/helpers/fzf/src/util/util.go +138 -0
  87. data/lib/helpers/fzf/src/util/util_test.go +40 -0
  88. data/lib/helpers/fzf/src/util/util_unix.go +47 -0
  89. data/lib/helpers/fzf/src/util/util_windows.go +83 -0
  90. data/lib/helpers/fzf/test/fzf.vader +175 -0
  91. data/lib/helpers/fzf/test/test_go.rb +2626 -0
  92. data/lib/helpers/fzf/uninstall +117 -0
  93. metadata +87 -1
@@ -0,0 +1,638 @@
1
+ package fzf
2
+
3
+ import (
4
+ "bytes"
5
+ "io"
6
+ "os"
7
+ "regexp"
8
+ "strings"
9
+ "testing"
10
+ "text/template"
11
+
12
+ "github.com/junegunn/fzf/src/util"
13
+ )
14
+
15
+ func TestReplacePlaceholder(t *testing.T) {
16
+ item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
17
+ items1 := []*Item{item1, item1}
18
+ items2 := []*Item{
19
+ newItem("foo'bar \x1b[31mbaz\x1b[m"),
20
+ newItem("foo'bar \x1b[31mbaz\x1b[m"),
21
+ newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
22
+
23
+ delim := "'"
24
+ var regex *regexp.Regexp
25
+
26
+ var result string
27
+ check := func(expected string) {
28
+ if result != expected {
29
+ t.Errorf("expected: %s, actual: %s", expected, result)
30
+ }
31
+ }
32
+ // helper function that converts template format into string and carries out the check()
33
+ checkFormat := func(format string) {
34
+ type quotes struct{ O, I, S string } // outer, inner quotes, print separator
35
+ unixStyle := quotes{`'`, `'\''`, "\n"}
36
+ windowsStyle := quotes{`^"`, `'`, "\n"}
37
+ var effectiveStyle quotes
38
+
39
+ if util.IsWindows() {
40
+ effectiveStyle = windowsStyle
41
+ } else {
42
+ effectiveStyle = unixStyle
43
+ }
44
+
45
+ expected := templateToString(format, effectiveStyle)
46
+ check(expected)
47
+ }
48
+ printsep := "\n"
49
+
50
+ /*
51
+ Test multiple placeholders and the function parameters.
52
+ */
53
+
54
+ // {}, preserve ansi
55
+ result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
56
+ checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
57
+
58
+ // {}, strip ansi
59
+ result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
60
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
61
+
62
+ // {}, with multiple items
63
+ result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
64
+ checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
65
+
66
+ // {..}, strip leading whitespaces, preserve ansi
67
+ result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
68
+ checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
69
+
70
+ // {..}, strip leading whitespaces, strip ansi
71
+ result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
72
+ checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
73
+
74
+ // {q}
75
+ result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
76
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
77
+
78
+ // {q}, multiple items
79
+ result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
80
+ checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
81
+
82
+ result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
83
+ checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
84
+
85
+ result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
86
+ checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
87
+
88
+ result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
89
+ checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
90
+
91
+ result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
92
+ checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
93
+
94
+ // forcePlus
95
+ result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
96
+ checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
97
+
98
+ // Whitespace preserving flag with "'" delimiter
99
+ result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
100
+ checkFormat("echo {{.O}} foo{{.O}}")
101
+
102
+ result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
103
+ checkFormat("echo {{.O}}bar baz{{.O}}")
104
+
105
+ result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
106
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
107
+
108
+ result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
109
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
110
+
111
+ // Whitespace preserving flag with regex delimiter
112
+ regex = regexp.MustCompile(`\w+`)
113
+
114
+ result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
115
+ checkFormat("echo {{.O}} {{.O}}")
116
+
117
+ result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
118
+ checkFormat("echo {{.O}}{{.I}}{{.O}}")
119
+
120
+ result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
121
+ checkFormat("echo {{.O}} {{.O}}")
122
+
123
+ // No match
124
+ result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
125
+ check("echo /")
126
+
127
+ // No match, but with selections
128
+ result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
129
+ checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
130
+
131
+ // String delimiter
132
+ result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
133
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
134
+
135
+ // Regex delimiter
136
+ regex = regexp.MustCompile("[oa]+")
137
+ // foo'bar baz
138
+ result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
139
+ checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
140
+
141
+ /*
142
+ Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
143
+ see: TestParsePlaceholder
144
+ */
145
+ items3 := []*Item{
146
+ // single line
147
+ newItem("1a 1b 1c 1d 1e 1f"),
148
+ // multi line
149
+ newItem("1a 1b 1c 1d 1e 1f"),
150
+ newItem("2a 2b 2c 2d 2e 2f"),
151
+ newItem("3a 3b 3c 3d 3e 3f"),
152
+ newItem("4a 4b 4c 4d 4e 4f"),
153
+ newItem("5a 5b 5c 5d 5e 5f"),
154
+ newItem("6a 6b 6c 6d 6e 6f"),
155
+ newItem("7a 7b 7c 7d 7e 7f"),
156
+ }
157
+ stripAnsi := false
158
+ printsep = "\n"
159
+ forcePlus := false
160
+ query := "sample query"
161
+
162
+ templateToOutput := make(map[string]string)
163
+ templateToFile := make(map[string]string) // same as above, but the file contents will be matched
164
+ // I. item type placeholder
165
+ templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
166
+ templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
167
+ templateToOutput[`{n}`] = `0`
168
+ templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
169
+ templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
170
+ templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
171
+ templateToFile[`{nf}`] = `0{{.S}}`
172
+ templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
173
+
174
+ // II. token type placeholders
175
+ templateToOutput[`{..}`] = templateToOutput[`{}`]
176
+ templateToOutput[`{1..}`] = templateToOutput[`{}`]
177
+ templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
178
+ templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
179
+ templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
180
+ // shorthand for x..x range
181
+ templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
182
+ templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
183
+ templateToOutput[`{-6}`] = templateToOutput[`{1}`]
184
+ // multiple ranges
185
+ templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
186
+ templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
187
+ templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
188
+ templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
189
+ // flags
190
+ templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
191
+ templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
192
+ templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
193
+ templateToFile[`{f1}`] = `1a{{.S}}`
194
+ templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
195
+ templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
196
+
197
+ // III. query type placeholder
198
+ // query flag is not removed after parsing, so it gets doubled
199
+ // while the double q is invalid, it is useful here for testing purposes
200
+ templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
201
+
202
+ // IV. escaping placeholder
203
+ templateToOutput[`\{}`] = `{}`
204
+ templateToOutput[`\{++}`] = `{++}`
205
+ templateToOutput[`{++}`] = templateToOutput[`{+}`]
206
+
207
+ for giveTemplate, wantOutput := range templateToOutput {
208
+ result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
209
+ checkFormat(wantOutput)
210
+ }
211
+ for giveTemplate, wantOutput := range templateToFile {
212
+ path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
213
+
214
+ data, err := readFile(path)
215
+ if err != nil {
216
+ t.Errorf("Cannot read the content of the temp file %s.", path)
217
+ }
218
+ result = string(data)
219
+
220
+ checkFormat(wantOutput)
221
+ }
222
+ }
223
+
224
+ func TestQuoteEntry(t *testing.T) {
225
+ type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
226
+ unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
227
+ windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
228
+ var effectiveStyle quotes
229
+
230
+ if util.IsWindows() {
231
+ effectiveStyle = windowsStyle
232
+ } else {
233
+ effectiveStyle = unixStyle
234
+ }
235
+
236
+ tests := map[string]string{
237
+ `'`: `{{.O}}{{.SQ}}{{.O}}`,
238
+ `"`: `{{.O}}{{.DQ}}{{.O}}`,
239
+ `\`: `{{.O}}{{.BS}}{{.O}}`,
240
+ `\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
241
+ `"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
242
+
243
+ `$`: `{{.O}}${{.O}}`,
244
+ `$HOME`: `{{.O}}$HOME{{.O}}`,
245
+ `'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
246
+
247
+ `&`: `{{.O}}{{.E}}&{{.O}}`,
248
+ `|`: `{{.O}}{{.E}}|{{.O}}`,
249
+ `<`: `{{.O}}{{.E}}<{{.O}}`,
250
+ `>`: `{{.O}}{{.E}}>{{.O}}`,
251
+ `(`: `{{.O}}{{.E}}({{.O}}`,
252
+ `)`: `{{.O}}{{.E}}){{.O}}`,
253
+ `@`: `{{.O}}{{.E}}@{{.O}}`,
254
+ `^`: `{{.O}}{{.E}}^{{.O}}`,
255
+ `%`: `{{.O}}{{.E}}%{{.O}}`,
256
+ `!`: `{{.O}}{{.E}}!{{.O}}`,
257
+ `%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
258
+ `C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
259
+ `"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
260
+ }
261
+
262
+ for input, expected := range tests {
263
+ escaped := quoteEntry(input)
264
+ expected = templateToString(expected, effectiveStyle)
265
+ if escaped != expected {
266
+ t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
267
+ }
268
+ }
269
+ }
270
+
271
+ // purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
272
+ func TestUnixCommands(t *testing.T) {
273
+ if util.IsWindows() {
274
+ t.SkipNow()
275
+ }
276
+ tests := []testCase{
277
+ // reference: give{template, query, items}, want{output OR match}
278
+
279
+ // 1) working examples
280
+
281
+ // paths that does not have to evaluated will work fine, when quoted
282
+ {give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
283
+ {give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
284
+ {give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
285
+
286
+ // only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
287
+ // quoting the tilde is required (to be treated as string)
288
+ {give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
289
+
290
+ // 2) problematic examples
291
+ // (not necessarily unexpected)
292
+
293
+ // paths that need to expand some part of it won't work (special characters and variables)
294
+ {give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
295
+ {give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
296
+ }
297
+ testCommands(t, tests)
298
+ }
299
+
300
+ // purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
301
+ func TestWindowsCommands(t *testing.T) {
302
+ if !util.IsWindows() {
303
+ t.SkipNow()
304
+ }
305
+ tests := []testCase{
306
+ // reference: give{template, query, items}, want{output OR match}
307
+
308
+ // 1) working examples
309
+
310
+ // example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
311
+ {give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
312
+ {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
313
+ // example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
314
+ {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
315
+ // example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
316
+ {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
317
+
318
+ // 2) problematic examples
319
+ // (not necessarily unexpected)
320
+
321
+ // notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
322
+ {give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
323
+
324
+ // cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
325
+ // cat: "C:\\test.txt: Invalid argument
326
+ {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
327
+ // cat: "C:\\test.txt": Invalid argument
328
+ {give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
329
+
330
+ // the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
331
+ // the temp file contains: `cat "C:\test.txt"`
332
+ // TODO this should actually work
333
+ {give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
334
+ }
335
+ testCommands(t, tests)
336
+ }
337
+
338
+ // purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
339
+ func TestPowershellCommands(t *testing.T) {
340
+ if !util.IsWindows() {
341
+ t.SkipNow()
342
+ }
343
+
344
+ tests := []testCase{
345
+ // reference: give{template, query, items}, want{output OR match}
346
+
347
+ /*
348
+ You can read each line in the following table as a pipeline that
349
+ consist of series of parsers that act upon your input (col. 1) and
350
+ each cell represents the output value.
351
+
352
+ For example:
353
+ - exec.Command("program.exe", `\''`)
354
+ - goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
355
+ - powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
356
+ - native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
357
+ - some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
358
+
359
+ Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
360
+ ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
361
+ " empty string² missing argument error ... ... |
362
+ \" literal " unbalanced quote error ... ... |
363
+ '\"' literal '"' literal " empty string empty string (match all) | yes
364
+ '\\\"' literal '\"' literal \" literal " literal " |
365
+ ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
366
+ \ transparent transparent transparent regex error |
367
+ '\' transparent literal \ literal \ regex error | yes
368
+ \\ transparent transparent transparent literal \ |
369
+ '\\' transparent literal \\ literal \\ literal \ |
370
+ ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
371
+ ' transparent unbalanced quote error ... ... |
372
+ \' transparent literal \ and unb. quote error ... ... |
373
+ \'' transparent literal \ and empty string literal \ regex error | no, but given as example above
374
+ ''' transparent unbalanced quote error ... ... |
375
+ '''' transparent literal ' literal ' literal ' | yes
376
+ ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
377
+
378
+ ⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
379
+ ¹: native commands like grep, git grep, ripgrep
380
+ ²: interpreted as a grouping quote, affects argument parser and gets removed from the result
381
+
382
+ [CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
383
+ [NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
384
+ */
385
+
386
+ // 1) working examples
387
+
388
+ {give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
389
+ {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
390
+
391
+ // example of escaping single quotes
392
+ {give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
393
+
394
+ // chaining powershells
395
+ {give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
396
+
397
+ // 2) problematic examples
398
+ // (not necessarily unexpected)
399
+
400
+ // looking for a path string will only work with escaped backslashes
401
+ {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
402
+ // looking for a literal double quote will only work with triple escaped double quotes
403
+ {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
404
+
405
+ // Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
406
+ // Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
407
+ {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
408
+
409
+ // the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
410
+ // to explorer, which will prompt user to pick editing program for the fzf-preview file
411
+ // the temp file contains: `cat "C:\test.txt"`
412
+ // TODO this should actually work
413
+ {give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
414
+ }
415
+
416
+ // to force powershell-style escaping we temporarily set environment variable that fzf honors
417
+ shellBackup := os.Getenv("SHELL")
418
+ os.Setenv("SHELL", "powershell")
419
+ testCommands(t, tests)
420
+ os.Setenv("SHELL", shellBackup)
421
+ }
422
+
423
+ /*
424
+ Test typical valid placeholders and parsing of them.
425
+
426
+ Also since the parser assumes the input is matched with `placeholder` regex,
427
+ the regex is tested here as well.
428
+ */
429
+ func TestParsePlaceholder(t *testing.T) {
430
+ // give, want pairs
431
+ templates := map[string]string{
432
+ // I. item type placeholder
433
+ `{}`: `{}`,
434
+ `{+}`: `{+}`,
435
+ `{n}`: `{n}`,
436
+ `{+n}`: `{+n}`,
437
+ `{f}`: `{f}`,
438
+ `{+nf}`: `{+nf}`,
439
+
440
+ // II. token type placeholders
441
+ `{..}`: `{..}`,
442
+ `{1..}`: `{1..}`,
443
+ `{..2}`: `{..2}`,
444
+ `{1..2}`: `{1..2}`,
445
+ `{-2..-1}`: `{-2..-1}`,
446
+ // shorthand for x..x range
447
+ `{1}`: `{1}`,
448
+ `{1..1}`: `{1..1}`,
449
+ `{-6}`: `{-6}`,
450
+ // multiple ranges
451
+ `{1,2}`: `{1,2}`,
452
+ `{1,2,4}`: `{1,2,4}`,
453
+ `{1,2..4}`: `{1,2..4}`,
454
+ `{1..2,-4..-3}`: `{1..2,-4..-3}`,
455
+ // flags
456
+ `{+1}`: `{+1}`,
457
+ `{+-1}`: `{+-1}`,
458
+ `{s1}`: `{s1}`,
459
+ `{f1}`: `{f1}`,
460
+ `{+s1..2}`: `{+s1..2}`,
461
+ `{+sf1..2}`: `{+sf1..2}`,
462
+
463
+ // III. query type placeholder
464
+ // query flag is not removed after parsing, so it gets doubled
465
+ // while the double q is invalid, it is useful here for testing purposes
466
+ `{q}`: `{qq}`,
467
+
468
+ // IV. escaping placeholder
469
+ `\{}`: `{}`,
470
+ `\{++}`: `{++}`,
471
+ `{++}`: `{+}`,
472
+ }
473
+
474
+ for giveTemplate, wantTemplate := range templates {
475
+ if !placeholder.MatchString(giveTemplate) {
476
+ t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
477
+ continue
478
+ }
479
+
480
+ _, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
481
+ gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
482
+
483
+ if gotTemplate != wantTemplate {
484
+ t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
485
+ }
486
+ }
487
+ }
488
+
489
+ /* utilities section */
490
+
491
+ // Item represents one line in fzf UI. Usually it is relative path to files and folders.
492
+ func newItem(str string) *Item {
493
+ bytes := []byte(str)
494
+ trimmed, _, _ := extractColor(str, nil, nil)
495
+ return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
496
+ }
497
+
498
+ // Functions tested in this file require array of items (allItems). The array needs
499
+ // to consist of at least two nils. This is helper function.
500
+ func newItems(str ...string) []*Item {
501
+ result := make([]*Item, util.Max(len(str), 2))
502
+ for i, s := range str {
503
+ result[i] = newItem(s)
504
+ }
505
+ return result
506
+ }
507
+
508
+ // (for logging purposes)
509
+ func (item *Item) String() string {
510
+ return item.AsString(true)
511
+ }
512
+
513
+ // Helper function to parse, execute and convert "text/template" to string. Panics on error.
514
+ func templateToString(format string, data interface{}) string {
515
+ bb := &bytes.Buffer{}
516
+
517
+ err := template.Must(template.New("").Parse(format)).Execute(bb, data)
518
+ if err != nil {
519
+ panic(err)
520
+ }
521
+
522
+ return bb.String()
523
+ }
524
+
525
+ // ad hoc types for test cases
526
+ type give struct {
527
+ template string
528
+ query string
529
+ allItems []*Item
530
+ }
531
+ type want struct {
532
+ /*
533
+ Unix:
534
+ The `want.output` string is supposed to be formatted for evaluation by
535
+ `sh -c command` system call.
536
+
537
+ Windows:
538
+ The `want.output` string is supposed to be formatted for evaluation by
539
+ `cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
540
+ behaviour, which is more favourable for nesting (possibly escaped)
541
+ special characters. This is the relevant section of `help cmd`:
542
+
543
+ ...old behavior is to see if the first character is
544
+ a quote character and if so, strip the leading character and
545
+ remove the last quote character on the command line, preserving
546
+ any text after the last quote character.
547
+ */
548
+ output string // literal output
549
+ match string // output is matched against this regex (when output is empty string)
550
+ }
551
+ type testCase struct {
552
+ give
553
+ want
554
+ }
555
+
556
+ func testCommands(t *testing.T, tests []testCase) {
557
+ // common test parameters
558
+ delim := "\t"
559
+ delimiter := Delimiter{str: &delim}
560
+ printsep := ""
561
+ stripAnsi := false
562
+ forcePlus := false
563
+
564
+ // evaluate the test cases
565
+ for idx, test := range tests {
566
+ gotOutput := replacePlaceholder(
567
+ test.give.template, stripAnsi, delimiter, printsep, forcePlus,
568
+ test.give.query,
569
+ test.give.allItems)
570
+ switch {
571
+ case test.want.output != "":
572
+ if gotOutput != test.want.output {
573
+ t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
574
+ idx,
575
+ test.give.template, test.give.query, test.give.allItems,
576
+ gotOutput, test.want.output)
577
+ }
578
+ case test.want.match != "":
579
+ wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
580
+ wantRegex := regexp.MustCompile(wantMatch)
581
+ if !wantRegex.MatchString(gotOutput) {
582
+ t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
583
+ idx,
584
+ test.give.template, test.give.query, test.give.allItems,
585
+ gotOutput, test.want.match)
586
+ }
587
+ default:
588
+ t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
589
+ }
590
+ }
591
+ }
592
+
593
+ // naive encoder of placeholder flags
594
+ func (flags placeholderFlags) encodePlaceholder() string {
595
+ encoded := ""
596
+ if flags.plus {
597
+ encoded += "+"
598
+ }
599
+ if flags.preserveSpace {
600
+ encoded += "s"
601
+ }
602
+ if flags.number {
603
+ encoded += "n"
604
+ }
605
+ if flags.file {
606
+ encoded += "f"
607
+ }
608
+ if flags.query {
609
+ encoded += "q"
610
+ }
611
+ return encoded
612
+ }
613
+
614
+ // can be replaced with os.ReadFile() in go 1.16+
615
+ func readFile(path string) ([]byte, error) {
616
+ file, err := os.Open(path)
617
+ if err != nil {
618
+ return nil, err
619
+ }
620
+ defer file.Close()
621
+
622
+ data := make([]byte, 0, 128)
623
+ for {
624
+ if len(data) >= cap(data) {
625
+ d := append(data[:cap(data)], 0)
626
+ data = d[:len(data)]
627
+ }
628
+
629
+ n, err := file.Read(data[len(data):cap(data)])
630
+ data = data[:len(data)+n]
631
+ if err != nil {
632
+ if err == io.EOF {
633
+ err = nil
634
+ }
635
+ return data, err
636
+ }
637
+ }
638
+ }
@@ -0,0 +1,26 @@
1
+ // +build !windows
2
+
3
+ package fzf
4
+
5
+ import (
6
+ "os"
7
+ "os/signal"
8
+ "strings"
9
+ "syscall"
10
+ )
11
+
12
+ func notifyOnResize(resizeChan chan<- os.Signal) {
13
+ signal.Notify(resizeChan, syscall.SIGWINCH)
14
+ }
15
+
16
+ func notifyStop(p *os.Process) {
17
+ p.Signal(syscall.SIGSTOP)
18
+ }
19
+
20
+ func notifyOnCont(resizeChan chan<- os.Signal) {
21
+ signal.Notify(resizeChan, syscall.SIGCONT)
22
+ }
23
+
24
+ func quoteEntry(entry string) string {
25
+ return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
26
+ }