├── README.md
├── UNLICENSE
├── sort-selections.kak
└── test.kak_
/README.md:
--------------------------------------------------------------------------------
1 | # kakoune-sort-selections
2 |
3 | [kakoune](http://kakoune.org) plugin to sort selections by their content.
4 |
5 | ## Setup
6 |
7 | Add `sort-selections.kak` to your autoload dir: `~/.config/kak/autoload/`, or source it manually.
8 |
9 | ## Usage
10 |
11 | With multiple selections, call the `sort-selections` command. The selections are sorted lexicographically based on their content. If `-reverse` is specified, their order is... reversed.
12 |
13 | If a register is specified (using `-register`), the values in the register will be sorted instead, and the resulting order then applied to the selections.
14 | For example, if you want to do case-insensitive sorting, you can first use `` ` `` to switch selections to lowercase, copy them to the `"` register with `y`, undo with `u` and then call `sort-selections '"'`. Similarly, you can also strip whitespace, convert to ascii...
15 |
16 | This plugin also adds the following two commands based on `sort-selections`:
17 |
18 | * `reverse-selections`, which is just a shortcut for `sort-selections -reverse -register '#'`. Since `#` is the selection index register, it just works.
19 | * `shuffle-selections`, which randomizes the order of all the current selections.
20 |
21 | ## Tests
22 |
23 | The `test.kak_` file contains tests for the plugin. To execute these tests, simply run `kak -n -e 'source test.kak_ ; quit'`: if the kakoune instance stays open, the tests have somehow failed and the current state can be inspected.
24 |
25 |
26 | ## License
27 |
28 | Unlicense
29 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
--------------------------------------------------------------------------------
/sort-selections.kak:
--------------------------------------------------------------------------------
1 | provide-module sort-selections %{
2 |
3 | define-command sort-selections -params .. -docstring '
4 | sort-selections []: sort the selections based on their content
5 | Sorting is done numerically if possible, otherwise lexicographically
6 | Switches:
7 | -reverse: reverse the sort order
8 | -register : sort the register content instead, and apply the order to the selections
9 | the number of elements in the register must match the number of selections
10 | -force-numeric: force sorting by numeric order, and fail if not all input are numbers
11 | -force-lexicographic: force sorting by lexicographic order, even if all input are numbers
12 | -dry-run: only check if input parameters are valid, do not sort
13 | ' -shell-script-candidates %{
14 | printf '%s\n' -reverse -force-numeric -force-lexicographic -register -dry-run
15 | } %{
16 | try %{
17 | exec -draft ''
18 | } catch %{
19 | fail 'Only one selection, cannot sort'
20 | }
21 | eval %sh{
22 | reverse=0
23 | type=0 # 0=auto / 1=numeric / 2=lexicographic
24 | register=''
25 | dry_run=0
26 | while [ $# -ne 0 ]; do
27 | arg_num=$((arg_num + 1))
28 | arg=$1
29 | shift
30 | if [ "$arg" = '-reverse' ]; then
31 | reverse=1
32 | elif [ "$arg" = '-force-numeric' ]; then
33 | type=1
34 | elif [ "$arg" = '-force-lexicographic' ]; then
35 | type=2
36 | elif [ "$arg" = '-register' ]; then
37 | if [ $# -eq 0 ]; then
38 | echo 'fail "Missing argument to -register"'
39 | exit 1
40 | fi
41 | arg_num=$((arg_num + 1))
42 | register=$1
43 | [ "$register" = "'" ] && register="''"
44 | printf "nop -- %%reg'%s'\n" "$register"
45 | shift
46 | elif [ "$arg" = '-dry-run' ]; then
47 | dry_run=1
48 | else
49 | printf "fail \"Unrecognized argument '%%arg{%s}'\"" "$arg_num"
50 | exit 1
51 | fi
52 | done
53 | printf "sort-selections-impl '%s' '%s' '%s' '%s'" "$reverse" "$type" "$register" "$dry_run"
54 | }
55 | }
56 |
57 | define-command reverse-selections -docstring '
58 | reverse-selections: reverses the order of all selections
59 | ' %{ sort-selections -reverse -register '#' }
60 |
61 | define-command shuffle-selections -docstring '
62 | shuffle-selections: randomizes the order of all selections
63 | ' %{
64 | eval -save-regs '"' %{
65 | eval reg dquote %sh{ seq "$kak_selection_count" | shuf | tr '\n' ' ' }
66 | sort-selections -register dquote
67 | }
68 | }
69 |
70 | define-command sort-selections-impl -hidden -params 4 %{
71 | eval -save-regs '"' %sh{
72 | perl - "$1" "$2" "$3" "$4" <<'EOF'
73 | use strict;
74 | use warnings;
75 | use Scalar::Util "looks_like_number";
76 |
77 | my $reverse = shift;
78 | my $type = shift;
79 | my $register = shift;
80 | my $dry_run = shift;
81 |
82 | my $command_fifo_name = $ENV{"kak_command_fifo"};
83 | my $response_fifo_name = $ENV{"kak_response_fifo"};
84 |
85 | sub parse_shell_quoted {
86 | my $str = shift;
87 | my @res;
88 | my $elem = "";
89 | while (1) {
90 | if ($str !~ m/\G'([\S\s]*?)'/gc) {
91 | print("echo -debug error1");
92 | exit;
93 | }
94 | $elem .= $1;
95 | if ($str =~ m/\G *$/gc) {
96 | push(@res, $elem);
97 | $elem = "";
98 | last;
99 | } elsif ($str =~ m/\G\\'/gc) {
100 | $elem .= "'";
101 | } elsif ($str =~ m/\G */gc) {
102 | push(@res, $elem);
103 | $elem = "";
104 | } else {
105 | print("echo -debug error2");
106 | exit;
107 | }
108 | }
109 | return @res;
110 | }
111 |
112 | sub read_array {
113 | my $what = shift;
114 |
115 | open (my $command_fifo, '>', $command_fifo_name);
116 | print $command_fifo "echo -quoting shell -to-file $response_fifo_name -- $what";
117 | close($command_fifo);
118 |
119 | # slurp the response_fifo content
120 | open (my $response_fifo, '<', $response_fifo_name);
121 | my $response_quoted = do { local $/; <$response_fifo> };
122 | close($response_fifo);
123 | return parse_shell_quoted($response_quoted);
124 | }
125 |
126 | sub are_all_numbers {
127 | my $array_ref = shift;
128 | for my $val (@$array_ref) {
129 | if (not looks_like_number($val)) {
130 | return 0;
131 | }
132 | }
133 | return 1;
134 | }
135 |
136 | sub should_sort_by_number {
137 | my $wanted_sort_type = shift;
138 | my $array_ref = shift;
139 | if ($wanted_sort_type == 0) { # auto
140 | return are_all_numbers($array_ref);
141 | } elsif ($wanted_sort_type == 1) { # want numeric, need to check, can fail
142 | if (are_all_numbers($array_ref) == 1) {
143 | return 1;
144 | } else {
145 | return -1;
146 | }
147 | } elsif ($wanted_sort_type == 2) { # want lexicographic, no check
148 | return 0;
149 | } else {
150 | print("echo -debug error3");
151 | exit;
152 | }
153 | }
154 |
155 | my @selections = read_array("%val{selections}");
156 | my $by_number;
157 |
158 | if ($register eq '') {
159 | my @sorted;
160 | $by_number = should_sort_by_number($type, \@selections);
161 | if ($by_number == -1) {
162 | printf("fail 'The selections must all be valid numbers' ;");
163 | exit;
164 | }
165 | if ($dry_run == 0) {
166 | if ($reverse == 1) {
167 | if ($by_number == 1) {
168 | @sorted = sort { $b <=> $a; } @selections;
169 | } else {
170 | @sorted = sort { $b cmp $a; } @selections;
171 | }
172 | } else {
173 | if ($by_number == 1) {
174 | @sorted = sort { $a <=> $b; } @selections;
175 | } else {
176 | @sorted = sort { $a cmp $b; } @selections;
177 | }
178 | }
179 | print("reg dquote");
180 | for my $sel (@sorted) {
181 | $sel =~ s/'/''/g;
182 | print(" '$sel'");
183 | }
184 | print(" ;");
185 | print("exec R ;");
186 | }
187 | } else {
188 | my @indices = read_array("%reg'$register'");
189 |
190 | if (scalar(@indices) != scalar(@selections)) {
191 | print("fail 'The register must contain as many values as selections' ;");
192 | exit;
193 | }
194 | $by_number = should_sort_by_number($type, \@indices);
195 | if ($by_number == -1) {
196 | printf("fail 'The register values must all be valid numbers' ;");
197 | exit;
198 | }
199 | if ($dry_run == 0) {
200 | my @pairs;
201 | for my $i (0 .. scalar(@indices) - 1) {
202 | push(@pairs, [ $indices[$i], $selections[$i] ] );
203 | }
204 | my @sorted;
205 | if ($reverse == 1) {
206 | if ($by_number == 1) {
207 | @sorted = sort { @$b[0] <=> @$a[0]; } @pairs;
208 | } else {
209 | @sorted = sort { @$b[0] cmp @$a[0]; } @pairs;
210 | }
211 | } else {
212 | if ($by_number == 1) {
213 | @sorted = sort { @$a[0] <=> @$b[0]; } @pairs;
214 | } else {
215 | @sorted = sort { @$a[0] cmp @$b[0]; } @pairs;
216 | }
217 | }
218 | print("reg dquote");
219 | for my $pair (@sorted) {
220 | my $sel = @$pair[1];
221 | $sel =~ s/'/''/g;
222 | print(" '$sel'");
223 | }
224 | print(" ;");
225 | print("exec R ;");
226 | }
227 | }
228 |
229 | my $how = ($by_number == 1 ? "numerically" : "lexicographically");
230 | my $target = ($register eq '' ? "content" : "index");
231 | my $count = scalar(@selections);
232 | print("echo -markup '{Information}Sorted $count selections $how by $target");
233 | if ($dry_run != 0) {
234 | print(" (dry-run)");
235 | }
236 | print("' ;");
237 | EOF
238 | }
239 | }
240 |
241 | }
242 |
243 | require-module sort-selections
244 |
--------------------------------------------------------------------------------
/test.kak_:
--------------------------------------------------------------------------------
1 | try %{
2 | require-module sort-selections
3 | } catch %{
4 | source sort-selections.kak
5 | require-module sort-selections
6 | }
7 |
8 | define-command assert-buffer-content-is -params .. %{
9 | eval -draft %{
10 | exec '%H'
11 | eval %sh{
12 | if [ "$1" != "$kak_quoted_selections" ]; then
13 | printf 'fail "Check failed"\n'
14 | fi
15 | }
16 | }
17 | }
18 |
19 | edit -scratch *sort-selections-test-1*
20 |
21 | # do a simple sort (and reverse sort) of 'foo' 'bar' 'baz'
22 | exec ifoobarbaz
23 | exec '%s\w+'
24 | sort-selections
25 | assert-buffer-content-is "'bar baz foo'"
26 | sort-selections -reverse
27 | assert-buffer-content-is "'foo baz bar'"
28 | exec '%d'
29 |
30 | # do a simple sort (and reverse sort) of '10' '30' '0' '100'
31 | exec i10300100
32 | exec '%s\w+'
33 | sort-selections
34 | assert-buffer-content-is "'0 10 30 100'"
35 | sort-selections -reverse
36 | assert-buffer-content-is "'100 30 10 0'"
37 | exec '%d'
38 |
39 | # do a sort based on the register content (which is the number we yanked previously)
40 | exec ifoo2bar3baz1
41 | exec -save-regs '' '%s\dy' # yank the number
42 | exec '%s\w+' # select the entire words
43 | sort-selections -register 'dquote'
44 | assert-buffer-content-is "'baz1 foo2 bar3'"
45 | # re-select: since we didn't change the dquote register, the sort-selections call is not idempotent
46 | sort-selections -register 'dquote'
47 | assert-buffer-content-is "'bar3 baz1 foo2'"
48 | exec -save-regs '' '%s\dy' # re-yank the number
49 | exec '%s\w+'
50 | sort-selections -reverse -register 'dquote'
51 | assert-buffer-content-is "'bar3 foo2 baz1'"
52 | exec '%d'
53 |
54 | exec ifoobarbaz
55 | exec '%s\w+'
56 | reverse-selections
57 | assert-buffer-content-is "'baz bar foo'"
58 | reverse-selections
59 | assert-buffer-content-is "'foo bar baz'"
60 | exec '%d'
61 |
62 | delete-buffer
63 |
--------------------------------------------------------------------------------