| 6 | | " FIXME: Translate the following FIXMEs in English. |
| 7 | | " |
| 8 | | " FIXME: ドキュメント更新。 |
| 9 | | " |
| 10 | | " FIXME: s:do(): どのitemにもマッチしない入力であってもactionに渡す。 |
| 11 | | " 例えば今の状況ではtype fileで新規ファイルを開けないので、 |
| 12 | | " このような処理は必要である。 |
| 13 | | " |
| 14 | | " ただし、これをするならばどのtypeのactionを使うかが問題。 |
| 15 | | " s:preferred_typeが定義されている場合に限定か。 |
| 16 | | " |
| 17 | | " FIXME: より「賢い」ソーティング (優先順位: 低) |
| 18 | | " - パスっぽいものは最後のコンポーネントを重視するのはどうか? |
| 19 | | " - type buffer: フルパスと相対パスのどちらを使うべきか? |
| 20 | | " - パスっぽいものは相対パス優先の方がいい? |
| 21 | | " '/'の値は若い以上、今のままでは自動的に相対パスが軽視されることになる。 |
| 22 | | " |
| 23 | | " FIXME: strtrans()の代替作成。<Left>等を正しく変換するように。 |
| 24 | | " |
| 25 | | " FIXME: type file - stub. |
| 26 | | " FIXME: type directory - NIY. |
| 27 | | " |
| 28 | | " FIXME: 各種比較でのcase sensitivityの統一・見直し。 |
| 29 | | " |
| 30 | | " FIXME: 実装方法の選択 |
| 31 | | " (A) ins-completion-menuを利用(Omni completion) |
| 32 | | " + Insert modeの基本的な操作体系を利用可能。 |
| 33 | | " - 一部キーをmappingできないため、細かい情報を取れない。 |
| 34 | | " * 特に問題な点は |
| 35 | | " 「どの項目が選択されているか」と |
| 36 | | " 「選択状況が変化したか」を |
| 37 | | " 知ることができないこと。 |
| 38 | | " * このために |
| 39 | | " 「actionを<Tab>で選んで実行」というI/Fや、 |
| 40 | | " 「利用可能なactionを適宜表示」ということができない。 |
| 41 | | " - CursorMovedIのフックを利用しての検出のため、カオス過ぎる。 |
| 42 | | " 特にs:PROMPTはひどい発明。 |
| 43 | | " |
| 44 | | " (B) getchar()で自力管理 |
| 45 | | " + キー入力を全て把握できるため、ins-completion-menuの問題はない。 |
| 46 | | " - キー入力を自力で管理する以上、 |
| 47 | | " 基本的な操作体系も含めて実装しなければならない。 |
| 48 | | " * itemsの一覧表示はins-completion-menu以外の方法が取れる。 |
| 49 | | " 例えばtype毎に並べたitemsのうち、 |
| 50 | | " preferred以外はfoldする等。 |
| 51 | | " |
| 52 | | " buffuzzy/zapitの経験から(A)を利用していたが、 |
| 53 | | " itemsの表示はバッファを使ってどうとでもできるので、 |
| 54 | | " (B)も一考の価値はある。 |
| 55 | | " |
| 56 | | " ただ、(A)でもある程度の問題は解決してしまったため、一先ずは(A)で行く。 |
| 57 | | " (B)には(B)で魅力的なメリットがあるので、落ち着いたらそちらも考える。 |
| | 6 | " FIXME: auto-complete 1 component for each typing '/' (like bluewind). |
| | 7 | " FIXME: s:do(): Force action on unmatched pattern. |
| | 8 | " FIXME: more smart sorting: |
| | 9 | " - considering last component. |
| | 10 | " - type buffer: full path vs. relative path. |
| | 11 | " FIXME: review on case sensitivity. |
| | 12 | " FIXME: alternative implementation (getchar()), if necessary. |
| | 168 | return s:TRUE |
| | 169 | endfunction |
| | 170 | |
| | 171 | |
| | 172 | |
| | 173 | |
| | 174 | function! ku#custom_key(type_name, key, new_action_name) "{{{2 |
| | 175 | if !s:valid_type_name_p(a:type_name) |
| | 176 | \ || !has_key(s:types[a:type_name].actions, a:new_action_name) |
| | 177 | return '' |
| | 178 | endif |
| | 179 | |
| | 180 | let old_action_name = get(s:types[a:type_name].keys, a:key, '') |
| | 181 | let s:types[(a:type_name)].keys[(a:key)] = a:new_action_name |
| | 182 | return old_action_name |
| | 183 | endfunction |
| | 184 | |
| | 185 | |
| | 186 | |
| | 187 | |
| | 188 | function! ku#custom_action(type_name, action_name, new_action_function) "{{{2 |
| | 189 | if !s:valid_type_name_p(a:type_name) || !s:callable_p(a:new_action_function) |
| | 190 | return s:FALSE |
| | 191 | endif |
| | 192 | |
| | 193 | let s:types[a:type_name].actions[a:action_name] = a:new_action_function |
| 291 | | if type(item) != type('') |
| 292 | | let ActionFunction = (a:choose_p |
| 293 | | \ ? s:choose_action(item._ku_type.actions) |
| 294 | | \ : item._ku_type.actions[0].function) |
| 295 | | call ActionFunction(item) |
| | 299 | if type(item) == s:TYPE_DICTONARY |
| | 300 | let ActionFunction |
| | 301 | \ = (a:choose_p |
| | 302 | \ ? s:choose_action_for_item(item) |
| | 303 | \ : item._ku_type.actions[item._ku_type.keys['*default*']]) |
| | 304 | call s:apply(ActionFunction, [item]) |
| 341 | | let keys = s:KEYS_TO_START_COMPLETION |
| | 352 | " FIXME: path separator assumption |
| | 353 | if (!s:auto_directory_insertion_done_p) |
| | 354 | \ && line[-1:] == '/' |
| | 355 | \ && len(s:PROMPT) + 2 <= len(line) |
| | 356 | let text = s:text_for_auto_completion(line,s:complete(s:FALSE,line[:-2])) |
| | 357 | if text != '' |
| | 358 | call setline('.', text) |
| | 359 | let keys = "\<End>/" |
| | 360 | let s:auto_directory_insertion_done_p = s:TRUE |
| | 361 | else |
| | 362 | let keys = s:KEYS_TO_START_COMPLETION |
| | 363 | let s:auto_directory_insertion_done_p = s:FALSE |
| | 364 | endif |
| | 365 | else |
| | 366 | let keys = s:KEYS_TO_START_COMPLETION |
| | 367 | let s:auto_directory_insertion_done_p = s:FALSE |
| | 368 | endif |
| | 540 | endfunction |
| | 541 | |
| | 542 | |
| | 543 | |
| | 544 | |
| | 545 | function! s:text_for_auto_completion(line, items) "{{{2 |
| | 546 | " Note that a:line always ends with '/', because this function is always |
| | 547 | " called by typing '/'. |
| | 548 | " FIXME: path separator assumption. |
| | 549 | let line_components = split(a:line[len(s:PROMPT):], '/', s:TRUE) |
| | 550 | for item in a:items |
| | 551 | let item_components = split(item.word, '/', s:TRUE) |
| | 552 | |
| | 553 | if len(line_components) <= len(item_components) |
| | 554 | " Discard items which don't match the line from the head of them. |
| | 555 | " Note that line_components[-1] is always '' and line_components[-2] is |
| | 556 | " almost imperfect, so that they aren't used. |
| | 557 | let i = 0 |
| | 558 | for i in range(len(line_components) - 2) |
| | 559 | if line_components[i] != item_components[i] |
| | 560 | break |
| | 561 | endif |
| | 562 | endfor |
| | 563 | if line_components[i] != item_components[i] |
| | 564 | continue |
| | 565 | endif |
| | 566 | " OK |
| | 567 | elseif isdirectory(item.word) " for type file. |
| | 568 | " OK |
| | 569 | else |
| | 570 | continue |
| | 571 | endif |
| | 572 | |
| | 573 | return join(item_components[:len(line_components) - 2], '/') |
| | 574 | endfor |
| | 575 | return '' |
| 545 | | function! s:choose_action(actions) "{{{2 |
| 546 | | " FIXME: style of the menu message. |
| 547 | | echo 'Available actions are:' |
| 548 | | for action in a:actions |
| 549 | | echo printf('%s :: %s', strtrans(action.key), action.name) |
| | 619 | function! s:choose_action_for_item(item) "{{{2 |
| | 620 | call s:show_available_actions_message(a:item) |
| | 621 | |
| | 622 | let c = nr2char(getchar()) |
| | 623 | redraw " clear the menu message lines to avoid hit-enter prompt. |
| | 624 | |
| | 625 | if has_key(a:item._ku_type.keys, c) |
| | 626 | let name = a:item._ku_type.keys[c] |
| | 627 | if has_key(a:item._ku_type.actions, name) |
| | 628 | return a:item._ku_type.actions[name] |
| | 629 | endif |
| | 630 | endif |
| | 631 | |
| | 632 | echo 'The key' s:string(c) 'is not associated with any action' |
| | 633 | \ '-- nothing happened.' |
| | 634 | return function('s:nop') |
| | 635 | endfunction |
| | 636 | |
| | 637 | |
| | 638 | |
| | 639 | |
| | 640 | function! s:show_available_actions_message(item) "{{{2 |
| | 641 | let items = items(a:item._ku_type.keys) |
| | 642 | call map(items, '[v:val[1], v:val[0]]') |
| | 643 | call sort(items) |
| | 644 | call filter(items, 'v:val[1] !=# "*default*"') |
| | 645 | let keys = map(copy(items), 'v:val[1]') |
| | 646 | let names = map(copy(items), 'v:val[0]') |
| | 647 | let max_key_length = max(map(copy(keys), 'len(s:string(v:val))')) |
| | 648 | let max_name_length = max(map(copy(names), 'len(v:val)')) |
| | 649 | let padding = 3 |
| | 650 | let max_cell_length = max_key_length + 3 + max_name_length + padding |
| | 651 | let format = '%*s%*s - %-*s' |
| | 652 | |
| | 653 | let max_column = max([1, (&columns + padding - 1) / max_cell_length]) |
| | 654 | let max_column = min([max_column, 4]) |
| | 655 | let n = len(items) |
| | 656 | let max_row = n / max_column + (n % max_column != 0) |
| | 657 | |
| | 658 | echo printf('Available actions for %s (type %s) are:', |
| | 659 | \ a:item.word, a:item._ku_type.name) |
| | 660 | for r in range(max_row) |
| | 661 | let i = r |
| | 662 | echo '' |
| | 663 | while i < n |
| | 664 | echon printf(format, |
| | 665 | \ (i == r ? 0 : padding), '', |
| | 666 | \ max_key_length, s:string(items[i][1]), |
| | 667 | \ max_name_length, items[i][0]) |
| | 668 | let i += max_row |
| | 669 | endwhile |
| 552 | | |
| 553 | | let c = nr2char(getchar()) |
| 554 | | redraw " clear the menu message lines to avoid hit-enter prompt. |
| 555 | | |
| 556 | | for action in a:actions |
| 557 | | if c ==# action.key |
| 558 | | return action.function |
| | 672 | endfunction |
| | 673 | |
| | 674 | |
| | 675 | |
| | 676 | |
| | 677 | function! s:valid_type_name_p(name) "{{{2 |
| | 678 | return has_key(s:types, a:name) |
| | 679 | endfunction |
| | 680 | |
| | 681 | |
| | 682 | |
| | 683 | |
| | 684 | function! s:valid_type_definition_p(args) "{{{2 |
| | 685 | if type(a:args) != s:TYPE_DICTONARY |
| | 686 | return s:FALSE |
| | 687 | endif |
| | 688 | |
| | 689 | " -- 'name' |
| | 690 | if !s:has_valid_entry(a:args, 'name', s:TYPE_STRING) |
| | 691 | return s:FALSE |
| | 692 | endif |
| | 693 | if !(a:args.name =~# '^[a-z]\+$') | return s:FALSE | endif |
| | 694 | |
| | 695 | " -- 'gather' |
| | 696 | if !s:has_valid_entry(a:args, 'gather', s:TYPE_FUNCTION) |
| | 697 | return s:FALSE |
| | 698 | endif |
| | 699 | |
| | 700 | " -- 'initialize' |
| | 701 | if has_key(a:args, 'initialize') |
| | 702 | if !(type(a:args.initialize) == s:TYPE_FUNCTION) | return s:FALSE | endif |
| | 703 | else |
| | 704 | let a:args.initialize = function('s:nop') |
| | 705 | endif |
| | 706 | |
| | 707 | " -- 'keys' |
| | 708 | if !s:has_valid_entry(a:args, 'keys', s:TYPE_DICTONARY) |
| | 709 | return s:FALSE |
| | 710 | endif |
| | 711 | for k in keys(a:args.keys) |
| | 712 | if !(type(k) == s:TYPE_STRING && type(a:args.keys[k]) == s:TYPE_STRING) |
| | 713 | return s:FALSE |
| 561 | | |
| 562 | | echo 'The key' strtrans(c) 'is not associated with any action' |
| 563 | | \ '-- nothing happened.' |
| 564 | | return function('s:nop') |
| 565 | | endfunction |
| 566 | | |
| 567 | | |
| 568 | | |
| 569 | | |
| 570 | | function! s:valid_type_name_p(name) "{{{2 |
| 571 | | for type_name in keys(s:types) |
| 572 | | if a:name ==# type_name |
| 573 | | return s:TRUE |
| | 716 | if !has_key(a:args.keys, '*default*') |
| | 717 | return s:FALSE |
| | 718 | endif |
| | 719 | |
| | 720 | " -- 'actions' |
| | 721 | if !s:has_valid_entry(a:args, 'actions', s:TYPE_DICTONARY) |
| | 722 | return s:FALSE |
| | 723 | endif |
| | 724 | for k in keys(a:args.actions) |
| | 725 | if !(type(k) == s:TYPE_STRING && s:callable_p(a:args.actions[k])) |
| | 726 | return s:FALSE |
| 576 | | return s:FALSE |
| 577 | | endfunction |
| 578 | | |
| 579 | | |
| 580 | | |
| 581 | | |
| 582 | | function! s:valid_type_definition_p(args) "{{{2 |
| 583 | | let T_NUM = type(0) |
| 584 | | let T_STR = type('') |
| 585 | | let T_FUNC = type(function('function')) |
| 586 | | let T_LIST = type([]) |
| 587 | | let T_DICT = type({}) |
| 588 | | |
| 589 | | if type(a:args) != T_DICT |
| 590 | | return s:FALSE |
| 591 | | endif |
| 592 | | |
| 593 | | " -- The name of this type. |
| 594 | | if !s:has_valid_entry(a:args, 'name', T_STR) | return s:FALSE | endif |
| 595 | | if !(a:args.name =~# '^[a-z]\+$') | return s:FALSE | endif |
| 596 | | |
| 597 | | " -- Available actions for this type of items. |
| 598 | | " This is a list of definitions of actions. |
| 599 | | " It must contain at least 1 definition. |
| 600 | | " The 1st definition is the default action. |
| 601 | | " |
| 602 | | " Each definition is a dictionary with 3 entries: |
| 603 | | " 'key' The key to choose this action in <Plug>(ku-choose-action). |
| 604 | | " 'name' The name of the action. |
| 605 | | " 'function' The function of the action. It is called with one parameter, |
| 606 | | " the selected item (as described in :help complete-items). |
| 607 | | if !s:has_valid_entry(a:args, 'actions', T_LIST) | return s:FALSE | endif |
| 608 | | if !(1 <= len(a:args.actions)) | return s:FALSE | endif |
| 609 | | for v in a:args.actions |
| 610 | | if !s:has_valid_entry(v, 'key', T_STR) | return s:FALSE | endif |
| 611 | | if !s:has_valid_entry(v, 'name', T_STR) | return s:FALSE | endif |
| 612 | | if !s:has_valid_entry(v, 'function', T_FUNC) | return s:FALSE | endif |
| | 729 | for n in values(a:args.keys) |
| | 730 | if !has_key(a:args.actions, n) |
| | 731 | return s:FALSE |
| | 732 | endif |
| | 751 | function! s:callable_p(obj) "{{{2 |
| | 752 | return (type(a:obj) == s:TYPE_FUNCTION) |
| | 753 | \ || ((type(a:obj) == s:TYPE_DICTONARY) && has_key(a:obj, '__call__')) |
| | 754 | endfunction |
| | 755 | |
| | 756 | |
| | 757 | |
| | 758 | |
| | 759 | function! s:apply(obj, args) "{{{2 |
| | 760 | if type(a:obj) == s:TYPE_FUNCTION |
| | 761 | return call(a:obj, a:args) |
| | 762 | elseif type(a:obj) == s:TYPE_DICTONARY && has_key(a:obj, '__call__') |
| | 763 | return call(a:obj.__call__, a:args, a:obj) |
| | 764 | else |
| | 765 | throw 'This object is not callable:' string(a:obj) |
| | 766 | endif |
| | 767 | endfunction |
| | 768 | |
| | 769 | |
| | 770 | |
| | 771 | |
| | 772 | function! s:pa(f, ...) "{{{2 |
| | 773 | " pa = Partial Apply |
| | 774 | " Returns a callable object g, |
| | 775 | " where g(b, ...) is equivalent to f(a, ..., b, ...). |
| | 776 | let g = {} |
| | 777 | let g.f = a:f |
| | 778 | let g.args = copy(a:000) " a:000 will be lost after this execution. |
| | 779 | function! g.__call__(...) |
| | 780 | return call(self.f, self.args + a:000) |
| | 781 | endfunction |
| | 782 | return g |
| | 783 | endfunction |
| | 784 | |
| | 785 | |
| | 786 | |
| | 787 | |
| | 793 | " common definitions "{{{2 |
| | 794 | function! s:_type_any_action_ex(item) |
| | 795 | call feedkeys(':', 'n') |
| | 796 | call feedkeys(ku#bang(), 'n') |
| | 797 | call feedkeys(' ', 'n') |
| | 798 | call feedkeys(a:item.word, 'n') |
| | 799 | call feedkeys("\<C-b>", 'n') |
| | 800 | endfunction |
| | 801 | |
| | 802 | |
| | 803 | function! s:_type_any_action_xcd(cd_command, item) |
| | 804 | " FIXME: escape special characters. |
| | 805 | if isdirectory(a:item.word) |
| | 806 | execute a:cd_command a:item.word |
| | 807 | elseif filereadable(a:item.word) |
| | 808 | execute a:cd_command fnamemodify(a:item.word, ':h') |
| | 809 | else |
| | 810 | echo printf('Item %s (type: %s) is not a file or directory.', |
| | 811 | \ a:item.word, a:item._ku_type.name) |
| | 812 | endif |
| | 813 | endfunction |
| | 814 | |
| | 815 | |
| | 816 | |
| | 817 | |
| 666 | | function! s:_type_buffer_action_split_open(item) |
| 667 | | execute a:item._buffer_number 'sbuffer' |
| 668 | | endfunction |
| 669 | | function! s:_type_buffer_action_vsplit_open(item) |
| 670 | | execute 'vertical' a:item._buffer_number 'sbuffer' |
| 671 | | endfunction |
| 672 | | |
| 673 | | |
| 674 | | " FIXME: other variants: hjkl HJKL. |
| | 848 | function! s:_type_buffer_action_xsplit(modifier, item) |
| | 849 | execute a:modifier a:item._buffer_number 'sbuffer' |
| | 850 | endfunction |
| | 851 | function! s:_type_buffer_action_xdelete(command, item) |
| | 852 | execute a:item._buffer_number a:command.ku#bang() |
| | 853 | endfunction |
| | 854 | |
| | 855 | |
| | 859 | \ 'keys': { |
| | 860 | \ "*default*": 'open', |
| | 861 | \ "o": 'open', |
| | 862 | \ "\<C-o>": 'open', |
| | 863 | \ "n": 'split', |
| | 864 | \ "\<C-n>": 'split', |
| | 865 | \ "s": 'split', |
| | 866 | \ "\<C-s>": 'split', |
| | 867 | \ "v": 'vsplit', |
| | 868 | \ "\<C-v>": 'vsplit', |
| | 869 | \ "h": 'split-left', |
| | 870 | \ "\<C-h>": 'split-left', |
| | 871 | \ "H": 'split-far-left', |
| | 872 | \ "j": 'split-down', |
| | 873 | \ "\<C-j>": 'split-down', |
| | 874 | \ "J": 'split-bottom', |
| | 875 | \ "k": 'split-up', |
| | 876 | \ "\<C-k>": 'split-up', |
| | 877 | \ "K": 'split-top', |
| | 878 | \ "l": 'split-right', |
| | 879 | \ "\<C-l>": 'split-right', |
| | 880 | \ "L": 'split-far-right', |
| | 881 | \ ";": 'ex', |
| | 882 | \ ":": 'ex', |
| | 883 | \ "/": 'cd', |
| | 884 | \ "?": 'cd-local', |
| | 885 | \ "U": 'unload', |
| | 886 | \ "D": 'delete', |
| | 887 | \ "W": 'wipeout', |
| | 888 | \ }, |
| | 889 | \ 'actions': { |
| | 890 | \ 'open': |
| | 891 | \ function('s:_type_buffer_action_open'), |
| | 892 | \ 'split': |
| | 893 | \ s:pa('s:_type_buffer_action_xsplit', ''), |
| | 894 | \ 'vsplit': |
| | 895 | \ s:pa('s:_type_buffer_action_xsplit', 'vertical'), |
| | 896 | \ 'split-left': |
| | 897 | \ s:pa('s:_type_buffer_action_xsplit', 'vertical leftabove'), |
| | 898 | \ 'split-far-left': |
| | 899 | \ s:pa('s:_type_buffer_action_xsplit', 'vertical topleft'), |
| | 900 | \ 'split-down': |
| | 901 | \ s:pa('s:_type_buffer_action_xsplit', 'rightbelow'), |
| | 902 | \ 'split-bottom': |
| | 903 | \ s:pa('s:_type_buffer_action_xsplit', 'botright'), |
| | 904 | \ 'split-up': |
| | 905 | \ s:pa('s:_type_buffer_action_xsplit', 'leftabove'), |
| | 906 | \ 'split-top': |
| | 907 | \ s:pa('s:_type_buffer_action_xsplit', 'topleft'), |
| | 908 | \ 'split-right': |
| | 909 | \ s:pa('s:_type_buffer_action_xsplit', 'vertical rightbelow'), |
| | 910 | \ 'split-far-right': |
| | 911 | \ s:pa('s:_type_buffer_action_xsplit', 'vertical botright'), |
| | 912 | \ 'ex': |
| | 913 | \ function('s:_type_any_action_ex'), |
| | 914 | \ 'cd': |
| | 915 | \ s:pa('s:_type_any_action_xcd', 'cd'), |
| | 916 | \ 'cd-local': |
| | 917 | \ s:pa('s:_type_any_action_xcd', 'lcd'), |
| | 918 | \ 'unload': |
| | 919 | \ s:pa('s:_type_buffer_action_xdelete', 'bunload'), |
| | 920 | \ 'delete': |
| | 921 | \ s:pa('s:_type_buffer_action_xdelete', 'bdelete'), |
| | 922 | \ 'wipeout': |
| | 923 | \ s:pa('s:_type_buffer_action_xdelete', 'bwipeout'), |
| | 924 | \ }, |