doing 2.0.18 → 2.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +15 -5
  4. data/README.md +1 -1
  5. data/bin/doing +2 -18
  6. data/doing.gemspec +5 -4
  7. data/doing.rdoc +2 -2
  8. data/generate_completions.sh +3 -3
  9. data/lib/doing/cli_status.rb +6 -2
  10. data/lib/doing/completion/bash_completion.rb +185 -0
  11. data/lib/doing/completion/fish_completion.rb +175 -0
  12. data/lib/doing/completion/string.rb +17 -0
  13. data/lib/doing/completion/zsh_completion.rb +140 -0
  14. data/lib/doing/completion.rb +39 -0
  15. data/lib/doing/version.rb +1 -1
  16. data/lib/doing/wwid.rb +18 -6
  17. data/lib/doing.rb +1 -1
  18. data/lib/helpers/fzf/.goreleaser.yml +119 -0
  19. data/lib/helpers/fzf/.rubocop.yml +28 -0
  20. data/lib/helpers/fzf/ADVANCED.md +565 -0
  21. data/lib/helpers/fzf/BUILD.md +49 -0
  22. data/lib/helpers/fzf/CHANGELOG.md +1193 -0
  23. data/lib/helpers/fzf/Dockerfile +11 -0
  24. data/lib/helpers/fzf/LICENSE +21 -0
  25. data/lib/helpers/fzf/Makefile +166 -0
  26. data/lib/helpers/fzf/README-VIM.md +486 -0
  27. data/lib/helpers/fzf/README.md +712 -0
  28. data/lib/helpers/fzf/bin/fzf-tmux +233 -0
  29. data/lib/helpers/fzf/doc/fzf.txt +512 -0
  30. data/lib/helpers/fzf/go.mod +17 -0
  31. data/lib/helpers/fzf/go.sum +31 -0
  32. data/lib/helpers/fzf/install +382 -0
  33. data/lib/helpers/fzf/install.ps1 +65 -0
  34. data/lib/helpers/fzf/main.go +14 -0
  35. data/lib/helpers/fzf/man/man1/fzf-tmux.1 +68 -0
  36. data/lib/helpers/fzf/man/man1/fzf.1 +1001 -0
  37. data/lib/helpers/fzf/plugin/fzf.vim +1048 -0
  38. data/lib/helpers/fzf/shell/completion.bash +381 -0
  39. data/lib/helpers/fzf/shell/completion.zsh +329 -0
  40. data/lib/helpers/fzf/shell/key-bindings.bash +96 -0
  41. data/lib/helpers/fzf/shell/key-bindings.fish +172 -0
  42. data/lib/helpers/fzf/shell/key-bindings.zsh +114 -0
  43. data/lib/helpers/fzf/src/LICENSE +21 -0
  44. data/lib/helpers/fzf/src/algo/algo.go +884 -0
  45. data/lib/helpers/fzf/src/algo/algo_test.go +197 -0
  46. data/lib/helpers/fzf/src/algo/normalize.go +492 -0
  47. data/lib/helpers/fzf/src/ansi.go +409 -0
  48. data/lib/helpers/fzf/src/ansi_test.go +427 -0
  49. data/lib/helpers/fzf/src/cache.go +81 -0
  50. data/lib/helpers/fzf/src/cache_test.go +39 -0
  51. data/lib/helpers/fzf/src/chunklist.go +89 -0
  52. data/lib/helpers/fzf/src/chunklist_test.go +80 -0
  53. data/lib/helpers/fzf/src/constants.go +85 -0
  54. data/lib/helpers/fzf/src/core.go +351 -0
  55. data/lib/helpers/fzf/src/history.go +96 -0
  56. data/lib/helpers/fzf/src/history_test.go +68 -0
  57. data/lib/helpers/fzf/src/item.go +44 -0
  58. data/lib/helpers/fzf/src/item_test.go +23 -0
  59. data/lib/helpers/fzf/src/matcher.go +235 -0
  60. data/lib/helpers/fzf/src/merger.go +120 -0
  61. data/lib/helpers/fzf/src/merger_test.go +88 -0
  62. data/lib/helpers/fzf/src/options.go +1691 -0
  63. data/lib/helpers/fzf/src/options_test.go +457 -0
  64. data/lib/helpers/fzf/src/pattern.go +425 -0
  65. data/lib/helpers/fzf/src/pattern_test.go +209 -0
  66. data/lib/helpers/fzf/src/protector/protector.go +8 -0
  67. data/lib/helpers/fzf/src/protector/protector_openbsd.go +10 -0
  68. data/lib/helpers/fzf/src/reader.go +201 -0
  69. data/lib/helpers/fzf/src/reader_test.go +63 -0
  70. data/lib/helpers/fzf/src/result.go +243 -0
  71. data/lib/helpers/fzf/src/result_others.go +16 -0
  72. data/lib/helpers/fzf/src/result_test.go +159 -0
  73. data/lib/helpers/fzf/src/result_x86.go +16 -0
  74. data/lib/helpers/fzf/src/terminal.go +2832 -0
  75. data/lib/helpers/fzf/src/terminal_test.go +638 -0
  76. data/lib/helpers/fzf/src/terminal_unix.go +26 -0
  77. data/lib/helpers/fzf/src/terminal_windows.go +45 -0
  78. data/lib/helpers/fzf/src/tokenizer.go +253 -0
  79. data/lib/helpers/fzf/src/tokenizer_test.go +112 -0
  80. data/lib/helpers/fzf/src/tui/dummy.go +46 -0
  81. data/lib/helpers/fzf/src/tui/light.go +987 -0
  82. data/lib/helpers/fzf/src/tui/light_unix.go +110 -0
  83. data/lib/helpers/fzf/src/tui/light_windows.go +145 -0
  84. data/lib/helpers/fzf/src/tui/tcell.go +721 -0
  85. data/lib/helpers/fzf/src/tui/tcell_test.go +392 -0
  86. data/lib/helpers/fzf/src/tui/ttyname_unix.go +47 -0
  87. data/lib/helpers/fzf/src/tui/ttyname_windows.go +14 -0
  88. data/lib/helpers/fzf/src/tui/tui.go +625 -0
  89. data/lib/helpers/fzf/src/tui/tui_test.go +20 -0
  90. data/lib/helpers/fzf/src/util/atomicbool.go +34 -0
  91. data/lib/helpers/fzf/src/util/atomicbool_test.go +17 -0
  92. data/lib/helpers/fzf/src/util/chars.go +198 -0
  93. data/lib/helpers/fzf/src/util/chars_test.go +46 -0
  94. data/lib/helpers/fzf/src/util/eventbox.go +96 -0
  95. data/lib/helpers/fzf/src/util/eventbox_test.go +61 -0
  96. data/lib/helpers/fzf/src/util/slab.go +12 -0
  97. data/lib/helpers/fzf/src/util/util.go +138 -0
  98. data/lib/helpers/fzf/src/util/util_test.go +40 -0
  99. data/lib/helpers/fzf/src/util/util_unix.go +47 -0
  100. data/lib/helpers/fzf/src/util/util_windows.go +83 -0
  101. data/lib/helpers/fzf/test/fzf.vader +175 -0
  102. data/lib/helpers/fzf/test/test_go.rb +2626 -0
  103. data/lib/helpers/fzf/uninstall +117 -0
  104. data/scripts/generate_bash_completions.rb +6 -12
  105. data/scripts/generate_fish_completions.rb +7 -16
  106. data/scripts/generate_zsh_completions.rb +6 -15
  107. metadata +144 -9
@@ -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
+ }