├── 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 | --------------------------------------------------------------------------------