diff --git a/usr/share/kak/autoload/filetree.kak b/usr/share/kak/autoload/filetree.kak new file mode 100644 index 0000000..6bc086f --- /dev/null +++ b/usr/share/kak/autoload/filetree.kak @@ -0,0 +1,495 @@ +declare-option -hidden str filetree_script_path %val{source} + +provide-module filetree %{ + +declare-option -docstring "Name of the client in which all source code jumps will be executed" str jumpclient +declare-option -docstring "name of the client in which utilities display information" str toolsclient + +declare-option -hidden bool filetree_highlight_dirty +declare-option -hidden str filetree_root_directory +declare-option -hidden range-specs filetree_open_files + +declare-option int filetree_indentation_level 3 + +face global FileTreeOpenFiles black,yellow +face global FileTreePipesColor rgb:606060,default +face global FileTreeDirColor blue,default+b +face global FileTreeFileName default,default +face global FileTreeEmptyName black,red + +define-command filetree-switch-or-start -params .. -docstring ' +filetree-switch-or-start: switch to the *filetree* buffer. +If the buffer does not exist, or the current kakoune directory has changed, it is generated from +scratch. In this case, all arguments are forwarded to the ''filetree'' command. +' -shell-script-candidates %{ + printf '%s\n' -files-first -dirs-first -consider-gitignore -no-empty-dirs -show-hidden -depth './' */ +} %{ + try %{ + eval -try-client %opt{toolsclient} %{ + buffer *filetree* + eval %sh{ [ "$kak_opt_filetree_root_directory" != "$PWD" ] && printf 'fail' } + } + } catch %{ + filetree %arg{@} + } +} + +define-command filetree -params .. -docstring ' +filetree [] []: open an interactive buffer representing the current directory +Switches: + -files-first: for each level, show files before directories + -dirs-first: for each level, show directories before files + -consider-gitignore: do not show any entries matched by gitignore rules + -no-empty-dirs: do not show empty directories + -show-hidden: show hidden files and directories + -depth : only traverse the root directory up to directories deep (unlimited by default) + -only-dirs: only show directories, not files + -no-report: do not show footer after the tree +' -shell-script-candidates %{ + printf '%s\n' -files-first -dirs-first -consider-gitignore -no-empty-dirs \ + -show-hidden -depth -only-dirs -no-report './' */ +} %{ + eval -save-regs 't' %{ + eval %sh{ + sorting='' + prune='' + gitignore='' + depth='' + directory='' + hidden='' + only_dirs='' + no_report='' + + arg_num=0 + accept_switch='y' + while [ $# -ne 0 ]; do + arg_num=$((arg_num + 1)) + arg=$1 + shift + if [ $accept_switch = 'y' ]; then + got_switch='y' + if [ "$arg" = '-files-first' ]; then + sorting='--filesfirst' + elif [ "$arg" = '-dirs-first' ]; then + sorting='--dirsfirst' + elif [ "$arg" = '-no-empty-dirs' ]; then + prune='--prune' + elif [ "$arg" = '-show-hidden' ]; then + hidden='-a' + elif [ "$arg" = '-consider-gitignore' ]; then + gitignore='--gitignore' + elif [ "$arg" = '-only-dirs' ]; then + only_dirs="-P '*/'" + elif [ "$arg" = '-no-report' ]; then + no_report="--noreport" + elif [ "$arg" = '-depth' ]; then + if [ $# -eq 0 ]; then + echo 'fail "Missing argument to -depth"' + exit 1 + fi + arg_num=$((arg_num + 1)) + depth="-L $1" + shift + elif [ "$arg" = '--' ]; then + accept_switch='n' + else + got_switch='n' + fi + [ $got_switch = 'y' ] && continue + fi + if [ "$directory" != '' ]; then + printf "fail \"Unknown argument '%%arg{%s}'\"" "$arg_num" + exit 1 + elif [ "$arg" != '' ]; then + directory="$arg" + else + printf "fail \"Invalid directory '%%arg{%s}'\"" "$arg_num" + exit 1 + fi + done + [ "$directory" = '' ] && directory='.' + # strip trailing '/' + while [ "$directory" != "${directory%/}" ]; do + directory=${directory%/} + done + if [ ! -d "$directory" ]; then + printf "fail \"Directory '%%arg{%s}' does not exist\"" "$arg_num" + exit 1 + fi + fifo=$(mktemp -u) + mkfifo "$fifo" + # $kak_opt_filetree_indentation_level <- need to let the script access this var + perl_script="${kak_opt_filetree_script_path%/*}/filetree.perl" + (tree -p -v $only_dirs $no_report $hidden $sorting $prune $gitignore $depth "$directory" | perl "$perl_script" 'process' > "$fifo") < /dev/null > /dev/null 2>&1 & + printf "set-register t '%s'" "$fifo" + } + try %{ delete-buffer *filetree* } + set-option global filetree_highlight_dirty false + + edit -fifo %reg{t} *filetree* + set-option buffer filetree_root_directory %sh{ printf '%s' "$PWD" } + # put hook in double quotes to interpolate %reg{t} + hook -always -once buffer BufCloseFifo .* " + nop %%sh{ rm '%reg{t}' } + exec ged + set-option global filetree_highlight_dirty true + filetree-refresh-files-highlight + " + + # highlight tree part + add-highlighter buffer/ regex '^[│ ]*[├└]─* ' 0:FileTreePipesColor + # highlight directories (using the /) + add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )?([^\n]*?)/$' 1:FileTreeDirColor + add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )([^\n/]*?)$' 1:FileTreeFileName + add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )(\n)' 1:FileTreeEmptyName + add-highlighter buffer/ ranges filetree_open_files + + map buffer normal ': filetree-open-selected-files' + map buffer normal ': filetree-open-selected-files -create' + map buffer normal ': filetree-select-prev-sibling' + map buffer normal ': filetree-select-next-sibling' + map buffer normal ': filetree-select-parent-directory' + map buffer normal ': filetree-select-first-child' + } +} + +define-command filetree-select-open-files %{ + eval select -timestamp %sh{ + # TODO might not work with filenames containing ' + eval set -- "$kak_quoted_opt_filetree_open_files" + printf '''%s''' "$1" + shift + for val do + printf ' ''%s''' "${val%|*}" + done + } +} + +define-command filetree-create-sibling %{ + # TODO handle root dir: forbid entirely? + filetree-select-path-component + try %{ + exec -draft '/\z' + exec 'xyP' + exec -draft 'ghh/[├└]r├' + } catch %{ + exec 'xyp' + exec -draft 'ghh/[├└]r├' + } + exec 'ghh/[├└]─* l' + try %{ exec '\nGLd' } +} + +define-command filetree-create-child %{ + # TODO handle root dir + filetree-select-path-component + # fail if we're not on a directory + try %{ + exec -draft '/\z' + } catch %{ + fail 'Cannot create child of a regular file' + } + exec 'xyp' # copy line below + exec 'ghh/[├└]─* ' # select end of pipe from the new line + try %{ exec -draft 'l\nGLd' } # remove filename + exec 'yP' # copy-prepend it + exec 'r;k' # replace it with space, and select the first character + try %{ + exec 'jr│' # select the one just above: if it's ├, turn into │ + } catch %{ + exec 'r└j' # otherwise into └ + } + exec '/[├└]' # select the real pipe connector + try %{ + exec -draft 'j[├└]' # and depending on whether it's the last child + exec 'r├' # replace with ├ + } catch %{ + exec 'r└' # or └ + } + exec 'gll' +} + +define-command filetree-select-parent-directory -docstring ' +filetree-select-next-sibling: in the *filetree* buffer, select the parent directory of the current element +' %{ + eval -itersel %{ + try %{ + exec -draft ';ghH\n.' + } catch %{ + fail 'Already at top parent' + } + try %{ + exec ';x1s(^[│ ]+)[└├]─* ' + exec "^(?!%val{selection})([^\n]*?)/$" + filetree-select-path-component + } catch %{ + exec ggxH + } + } +} + +define-command filetree-select-next-sibling -docstring ' +filetree-select-next-sibling: in the *filetree* buffer, select the next element in the same directory +' %{ + eval -itersel -save-regs '/' %{ + exec ';x' + exec '1s^([ │]*)[└├]' + try %{ + exec '\A[└├]\z' + reg slash "\n(([^\n]*\n)*?(^[└├]))?" + } catch %{ + reg slash "\n((^%val{selection}[^\n]*\n)*?(^%val{selection}[└├]))?" + } + exec 'gh/' + exec '\A\n\z' # if we didn't match anything, fail + exec ';' + filetree-select-path-component + } +} +define-command filetree-select-prev-sibling -docstring ' +filetree-select-prev-sibling: in the *filetree* buffer, select the previous element in the same directory +' %{ + eval -itersel -save-regs '/' %{ + exec ';x' + exec '1s^([ │]*)[└├]' + try %{ + exec '\A[└├]\z' + reg slash "((^├[^\n]*\n)(^[^\n]*\n)*?)?^." + } catch %{ + reg slash "((^%val{selection}├[^\n]*\n)(^%val{selection}[^\n]*\n)*?)?^." + } + exec 'gl' + exec '\A^.\z' # if we didn't match anything, fail + exec ';' + filetree-select-path-component + } +} + +define-command filetree-select-first-child -docstring ' +filetree-select-first-child: in the *filetree* buffer, select the first element in the selected directory +' %{ + eval -itersel %{ + exec ';x' + exec -draft '/$' + try %{ + exec -draft 'ghH\A.\z' + exec 'j' + } catch %{ + exec 's^[ │]*[└├]─* ' + exec "jx\A^[│ ]{%val{selection_length}}" + } + filetree-select-path-component + } +} + +define-command filetree-select-direct-children -docstring ' +filetree-select-direct-children: in the *filetree* buffer, select all elements in the selected directory +' %{ + eval -itersel -save-regs 'l' %{ + exec ';x' + exec -draft '/$' + try %{ + exec -draft 'ghH\A.\z' + reg l '0' + exec gh + } catch %{ + exec 's^[ │]*[└├]─* ' + reg l %val{selection_length} + } + exec "gll/(^[│ ]{%reg{l}}[^\n]+\n)*|." + exec '\A.\z' + exec "^[│ ]{%reg{l}}[└├]" + filetree-select-path-component + } +} + +define-command filetree-select-all-children -docstring ' +filetree-select-all-children: in the *filetree* buffer, select all elements which are descendents of the selected directory +' %{ + eval -itersel -save-regs 'l' %{ + exec ';x' + exec -draft '/$' + try %{ + exec -draft 'ghH\A.\z' + reg l '0' + exec gh + } catch %{ + exec 's^[ │]*[└├]─* ' + reg l %val{selection_length} + } + exec "gll/(^[│ ]{%reg{l}}[^\n]*\n)*|." + exec '\A.\z' + exec '' + filetree-select-path-component + } +} + +define-command -hidden filetree-eval-on-fullpath -params 1 %{ + eval -save-regs 'p' %{ + eval -draft %{ + exec ',' + reg p %val{selection} + try %{ + # TODO not exactly elegant + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + filetree-select-parent-directory; reg p "%val{selection}%reg{p}" + } + } + eval %arg{1} + } +} + +define-command -hidden filetree-open-selected-file -params ..1 %{ + filetree-eval-on-fullpath %sh{ + printf '%s' "try %{ + buffer %reg{p} + } catch %{ + edit -existing %reg{p} + reg e \"x%reg{e}\" + } catch %{" + if [ "$1" = '-create' ]; then + printf '%s' " + edit %reg{p} + reg c \"x%reg{c}\" + } catch %{" + fi + printf '%s' "reg f \"x%reg{f}\" + }" + } +} + +define-command filetree-open-selected-files -params ..1 -docstring ' +filetree-open-selected-files [-create]: open the files currently selected +' -shell-script-candidates %{ + printf "%s\n" '-create' +} %{ + eval -save-regs 'cef' %{ + reg e '' + reg c '' + reg f '' + filetree-select-path-component + exec '/\z' + try %{ + # open all non-main selections in a draft context + eval -draft %{ + exec '' + eval -itersel %{ eval -draft -verbatim filetree-open-selected-file %arg{@} } + } + } + filetree-open-selected-file %arg{@} + eval %sh{ + # echo some "helpful" info + num_opened="${#kak_reg_e}" + num_created="${#kak_reg_c}" + num_failed="${#kak_reg_f}" + total=$(( num_opened + num_created + num_failed )) + [ "$total" -eq 0 ] && exit + + str_opened="${num_opened} existing file" + [ "$num_opened" -ne 1 ] && str_opened="${str_opened}s" + str_created="${num_created} new file" + [ "$num_created" -ne 1 ] && str_created="${str_created}s" + str_failed="${num_failed} file" + [ "$num_failed" -ne 1 ] && str_failed="${str_failed}s" + + printf "echo '" + if [ "$num_opened" -eq "$total" ]; then + printf "Opened %s" "$str_opened" + elif [ "$num_created" -eq "$total" ]; then + printf "Opened %s" "$str_created" + elif [ "$num_failed" -eq "$total" ]; then + printf "Failed to open %s" "$str_failed" + elif [ "$num_failed" -eq 0 ]; then # opened + created + printf "Opened %s, and %s" "$str_opened" "$str_created" + elif [ "$num_created" -eq 0 ]; then # opened + failed + printf "Opened %s, and failed to open %s" "$str_opened" "$str_failed" + elif [ "$num_opened" -eq 0 ]; then # created + failed + printf "Opened %s, and failed to open %s" "$str_created" "$str_failed" + else + printf "Opened %s, %s, and failed to open %s" "$str_opened" "$str_created" "$str_failed" + fi + printf "'" + } + } +} + +define-command filetree-select-path-component %{ + exec ';x1s^[│ ]*[├└]─* (.*)\n' +} + +hook global WinDisplay '^\*filetree\*$' %{ + try %{ + eval %sh{ [ "$kak_opt_filetree_highlight_dirty" = 'false' ] && printf 'fail' } + filetree-refresh-files-highlight + set global filetree_highlight_dirty false + } +} + +hook global BufCreate .* %{ set global filetree_highlight_dirty true } +hook global BufClose .* %{ set global filetree_highlight_dirty true } + +define-command -hidden filetree-refresh-files-highlight %{ + try %{ + eval -draft -buffer *filetree* %{ + eval select %sh{ + script="${kak_opt_filetree_script_path%/*}/filetree.perl" + echo "write '$kak_response_fifo'" > "$kak_command_fifo" + eval set -- "$kak_quoted_buflist" + perl "$script" 'match-buffers' "$@" < "$kak_response_fifo" + } + filetree-select-path-component + + set-option buffer filetree_open_files %val{timestamp} + eval -no-hooks -draft -itersel %{ + set -add buffer filetree_open_files "%val{selection_desc}|FileTreeOpenFiles" + } + } + } +} + +define-command filetree-edit -params 1.. -docstring ' +filetree-edit: edit the specified files. +The completions are provided by the *filetree* buffer. +' %{ + edit %arg{@} +} + +complete-command -menu filetree-edit shell-script-candidates %{ + fifo=$(mktemp -u) + mkfifo "$fifo" + echo "try %{ eval -buffer *filetree* %{ write '$fifo' } } catch %{ echo -to-file '$fifo' '' }" | kak -p "$kak_session" + perl "${kak_opt_filetree_script_path%/*}/filetree.perl" 'flatten-nodirs' < "$fifo" + rm "$fifo" +} + +define-command filetree-goto -params 1.. -docstring ' +filetree-goto: select the specified path elements in the *filetree* buffer +' %{ + buffer *filetree* + eval select %sh{ + script="${kak_opt_filetree_script_path%/*}/filetree.perl" + echo "write '$kak_response_fifo'" > "$kak_command_fifo" + perl "$script" 'match-buffers' "$@" < "$kak_response_fifo" + } + filetree-select-path-component +} + +complete-command -menu filetree-goto shell-script-candidates %{ + fifo=$(mktemp -u) + mkfifo "$fifo" + echo "try %{ eval -buffer *filetree* %{ write '$fifo' } } catch %{ echo -to-file '$fifo' '' }" | kak -p "$kak_session" + perl "${kak_opt_filetree_script_path%/*}/filetree.perl" 'flatten-all' < "$fifo" + rm "$fifo" +} + +} + +require-module filetree diff --git a/usr/share/kak/autoload/filetree.perl b/usr/share/kak/autoload/filetree.perl new file mode 100644 index 0000000..4433ef5 --- /dev/null +++ b/usr/share/kak/autoload/filetree.perl @@ -0,0 +1,135 @@ +use strict; +use warnings; + +my $operation = $ARGV[0]; +if (not defined($operation)) { + exit(3); +} +shift; + +sub read_line_by_line { + my $callback = $_[0]; + + my $padding_size = 0; + my @dir_stack; + my $line_count = 0; + + my $prev_depth = 0; + while (my $input = ) { + chomp($input); + if ($input eq "") { + last; + } elsif ($line_count == 0) { + if ($input ne './') { + push(@dir_stack, substr($input, 0, -1)); + } + $line_count = 1; + next; + } + + my $depth = 1; + if ($line_count == 1) { + # need to infer the width of the pipes, based on the first line + if ($input !~ m/\G(└|├)((─)* )/gc) { + exit(1); + } + # the pipe character is actually length '3' + $padding_size = (length($2) - 1) / 3; + } else { + while ($input =~ m/\G(│| ) {$padding_size} /gco) { + $depth += 1; + } + if ($input !~ m/\G(└|├)(─)* /gc) { + exit(1); + } + } + $line_count += 1; + + if ($depth <= $prev_depth) { + my $remove = $prev_depth - $depth + 1; + splice(@dir_stack, -$remove); + } elsif ($depth > $prev_depth + 1) { + # does not make sense to grow by >1 level + exit(1); + } + $input =~ m|\G(.*?)(/?)$|gc; + my $component = $1; + my $is_dir = ($2 eq '/'); + $prev_depth = $depth; + push(@dir_stack, $component); + $callback->(join('/', @dir_stack), $is_dir, $line_count); + } +} + +if ($operation eq "flatten-all") { + sub callback1 { + print("$_[0]\n"); + } + read_line_by_line(\&callback1); +} elsif ($operation eq "flatten-nodirs") { + sub callback2 { + if (not $_[1]) { + print("$_[0]\n"); + } + } + read_line_by_line(\&callback2); +} elsif ($operation eq "match-buffers") { + my %map; + for my $buf (@ARGV) { + $map{$buf} = 1; + } + sub callback3 { + my $path = $_[0]; + if (exists($map{$path})) { + my $line = $_[2]; + print("'$line.1,$line.1' "); + delete $map{$path}; + } + } + read_line_by_line(\&callback3); +} elsif ($operation eq "process") { + + my $repetition = int($ENV{"kak_opt_filetree_indentation_level"} or 3); + my $first = 1; + + while (my $input = ) { + chomp($input); + if ($input eq "") { + print("\n"); + last; + } + my $out = ""; + if ($first == 1) { + $first = 0; + } else { + while ($input =~ m/\G(?:(│)\xc2\xa0\xc2\xa0|( ) ) /gc) { + $out .= ($1 or $2) . ' ' x ($repetition + 1); + } + if ($input !~ m/\G(└|├)(─)─ /gc) { + exit(1); + } + $out .= $1 . $2 x $repetition . ' '; + } + my $type = ''; + if ($input =~ m/\G\[([-sdl]).{9}\] /gc) { + $type = $1; + } + if ($type eq 'l') { + $input =~ m/\G(.*) -> .*?$/gc; + $out .= $1; + } else { + $input =~ m/\G(.*)$/gc; + $out .= $1; + } + if ($type eq 'd') { + $out .= '/'; + } + print("$out\n"); + } + + my $last = ; + print("$last"); + +} else { + exit(2); +}