Skip to content

Instantly share code, notes, and snippets.

@kana
Created May 15, 2009 17:13
Show Gist options
  • Save kana/112311 to your computer and use it in GitHub Desktop.
Save kana/112311 to your computer and use it in GitHub Desktop.
" ku - An interface for anything
" Version: 0.2.2
" Copyright (C) 2008-2009 kana <http://whileimautomaton.net/>
" License: MIT license {{{
" Permission is hereby granted, free of charge, to any person obtaining
" a copy of this software and associated documentation files (the
" "Software"), to deal in the Software without restriction, including
" without limitation the rights to use, copy, modify, merge, publish,
" distribute, sublicense, and/or sell copies of the Software, and to
" permit persons to whom the Software is furnished to do so, subject to
" the following conditions:
"
" The above copyright notice and this permission notice shall be included
" in all copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
" OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
" IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
" CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
" TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
" SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
" }}}
" Coding Guidelines "{{{1
" Naming Guidelines "{{{2
"
" - Use the prefix "original_" and include the namaes of options/functions for
" variables which hold values from options/functions to restore the states
" of Vim before a ku session.
"
" For example, 'completeopt' is changed while a ku session, so that the
" original value of 'completeopt' must be saved and restored. To keep the
" original value of 'completeopt', use "original_completeopt".
" Variables "{{{1
" Global "{{{2
" Behavior about auto component completion.
if !exists('g:ku_acc_style')
let g:ku_acc_style = '' " A comma-separated list of words.
endif
" The name of the ku buffer.
if !exists('g:ku_buffer_name')
if has('win16') || has('win32') || has('win64') " on Microsoft Windows
let g:ku_buffer_name = '[ku]'
else
let g:ku_buffer_name = '*ku*'
endif
endif
if !exists('g:ku_choosing_actions_sorting_style')
let g:ku_choosing_actions_sorting_style = 'by-action'
endif
" Junk patterns.
" There may be g:ku_{source}_junk_pattern.
if !exists('g:ku_common_junk_pattern')
let g:ku_common_junk_pattern = ''
endif
" Special characters to activate automatic component completion.
if !exists('g:ku_component_separators')
let g:ku_component_separators = '/\:'
endif
if !exists('g:ku_history_added_p')
let g:ku_history_added_p = 'ku#_history_added_p'
endif
if !exists('g:ku_history_size')
let g:ku_history_size = 1000
endif
if !exists('g:ku_history_reloading_style')
let g:ku_history_reloading_style = 'idle'
endif
" Script-local "{{{2
" Misc. constants.
let s:FALSE = 0
let s:TRUE = !s:FALSE
" Magic line numbers in the ku buffer.
let s:LNUM_STATUS = 1
let s:LNUM_INPUT = 2
" Path separator.
let s:PATH_SEP = (exists('+shellslash') && !&shellslash) ? '\' : '/'
" The buffer number of the ku buffer.
let s:INVALID_BUFNR = -1
if exists('s:bufnr') && bufexists(s:bufnr)
execute s:bufnr 'bwipeout'
endif
let s:bufnr = s:INVALID_BUFNR
" The name of the current source.
let s:INVALID_SOURCE = '*invalid*'
let s:current_source = s:INVALID_SOURCE
" For automatic completion.
let s:KEYS_TO_START_COMPLETION = "\<C-x>\<C-o>\<C-p>"
let s:PROMPT = '>' " must be a single character.
let s:INVALID_COL = -3339
let s:last_col = s:INVALID_COL
let s:automatic_component_completion_done_p = s:FALSE
" To take action on the appropriate item.
let s:last_completed_items = []
" There are 2 versions for user input:
"
" raw Text which user inserts at the ku window.
" prefix-expanded User input, its prefix is expanded.
" (see ku#custom_prefix())
"
" Variables which hold user input are named with the following suffixes:
" "_raw" if variables hold raw version,
" "_ped" if variables hold prefix-expanded version.
"
" Note that user input in the ku window is always raw version. User will
" never see prefix-expanded version of user input in the ku window. This
" policy is to avoid recursive prefix expansion whenever user types in the
" ku window.
let s:last_user_input_raw = ''
" Information to restore several stuffs after a ku session.
let s:original_completeopt = ''
let s:original_curwinnr = 0
let s:original_ignorecase = ''
let s:original_winrestcmd = ''
" User defined action tables, key tables and prefix table for sources.
if !exists('s:custom_action_tables')
let s:custom_action_tables = {} " source -> action-table
endif
if !exists('s:custom_key_tables')
let s:custom_key_tables = {} " source -> key-table
endif
if !exists('s:custom_prefix_tables')
let s:custom_prefix_tables = {} " source -> prefix-table
endif
" Priorities table: source -> priority.
if !exists('s:priority_table')
let s:priority_table = {}
endif
let s:DEFAULT_PRIORITY = 500
let s:MIN_PRIORITY = 100
let s:MAX_PRIORITY = 999
" Session ID. A session is a period of time during the ku window is opened.
let s:session_id = 0
" For s:recall_input_history().
let s:current_hisotry_index = -1
" For ku#restart().
let s:last_used_source = s:INVALID_SOURCE
let s:last_used_input_pattern = ''
" Interface "{{{1
function! ku#available_source_p(source) "{{{2
return 0 <= index(ku#available_sources(), a:source)
endfunction
function! ku#available_sources() "{{{2
" Assumes that s:available_sources will be never changed during a session.
if s:ku_active_p() && s:session_id == s:_session_id_source_cache
return s:available_sources
endif
let s:available_sources = sort(s:calculate_available_sources())
let s:_session_id_source_cache = s:session_id
return s:available_sources
endfunction
if !exists('s:available_sources')
let s:available_sources = [] " [source-name, ...]
endif
let s:_session_id_source_cache = 0
function! s:calculate_available_sources()
let _ = []
for source_name_base
\ in map(s:runtime_files(ku#make_path('autoload', 'ku', '*.vim')),
\ 'fnamemodify(v:val, ":t:r")')
call extend(_, s:api_available_sources(source_name_base))
endfor
return _
endfunction
function! ku#command_complete(arglead, cmdline, cursorpos) "{{{2
return join(ku#available_sources(), "\n")
endfunction
function! ku#custom_action(source, action, ...) "{{{2
if !has_key(s:custom_action_tables, a:source)
let s:custom_action_tables[a:source] = {}
endif
if a:0 == 1
call call('s:ku_custom_action_3', [a:source, a:action] + a:000)
elseif a:0 == 2
call call('s:ku_custom_action_4', [a:source, a:action] + a:000)
else
echoerr printf('Invalid arguments: %s', string([a:source,a:action]+a:000))
endif
endfunction
function! s:ku_custom_action_3(source, action, function) "{{{3
let s:custom_action_tables[a:source][a:action] = a:function
endfunction
function! s:ku_custom_action_4(source, action, source2, action2) "{{{3
let action_table = (a:source2 !=# 'common'
\ ? s:api_action_table(a:source2)
\ : s:default_action_table())
let function2 = get(action_table, a:action2, 0)
if function2 is 0
echoerr printf('No such action for %s: %s', a:source2, string(a:action2))
return
endif
let s:custom_action_tables[a:source][a:action] = function2
endfunction
function! ku#custom_key(source, key, action) "{{{2
if !has_key(s:custom_key_tables, a:source)
let s:custom_key_tables[a:source] = {}
endif
let s:custom_key_tables[a:source][a:key] = a:action
endfunction
function! ku#custom_prefix(source, prefix, text) "{{{2
if !has_key(s:custom_prefix_tables, a:source)
let s:custom_prefix_tables[a:source] = {}
endif
let _ = s:custom_prefix_tables[a:source]
if a:text != ''
let _[a:prefix] = a:text
else
if has_key(_, a:prefix)
call remove(_, a:prefix)
endif
endif
endfunction
function! ku#custom_priority(source, priority) "{{{2
if type(a:priority) != type(0)
echoerr 'priority must be integer, but got:' string(a:priority)
return
endif
if a:priority < s:MIN_PRIORITY || s:MAX_PRIORITY < a:priority
echoerr 'priority is out of the range:' string(a:priority)
return
endif
let s:priority_table[a:source] = a:priority
endfunction
function! ku#default_key_mappings(override_p) "{{{2
let _ = a:override_p ? '' : '<unique>'
call s:ni_map(_, '<buffer> <C-c>', '<Plug>(ku-cancel)')
call s:ni_map(_, '<buffer> <Return>', '<Plug>(ku-do-the-default-action)')
call s:ni_map(_, '<buffer> <C-m>', '<Plug>(ku-do-the-default-action)')
call s:ni_map(_, '<buffer> <Tab>', '<Plug>(ku-choose-an-action)')
call s:ni_map(_, '<buffer> <C-i>', '<Plug>(ku-choose-an-action)')
call s:ni_map(_, '<buffer> <Esc>i', '<Plug>(ku-do-persistent-action)')
call s:ni_map(_, '<buffer> <C-j>', '<Plug>(ku-next-source)')
call s:ni_map(_, '<buffer> <C-k>', '<Plug>(ku-previous-source)')
call s:ni_map(_, '<buffer> <C-l>', '<Plug>(ku-choose-source)')
call s:ni_map(_, '<buffer> <Esc>l', '<Plug>(ku-history-source)')
call s:ni_map(_, '<buffer> <Esc>j', '<Plug>(ku-newer-history)')
call s:ni_map(_, '<buffer> <Esc>k', '<Plug>(ku-older-history)')
call s:ni_map(_, '<buffer> <Esc>J', '<Plug>(ku-newer-history-and-source)')
call s:ni_map(_, '<buffer> <Esc>K', '<Plug>(ku-older-history-and-source)')
return
endfunction
function! ku#do_action(name) "{{{2
if !s:ku_active_p()
echoerr 'ku is not active'
return s:FALSE
endif
return s:do(a:name)
endfunction
function! ku#get_the_current_input_pattern() "{{{2
if s:ku_active_p()
return s:remove_prompt(getline(s:LNUM_INPUT))
else
return 0
endif
endfunction
function! ku#input_history() "{{{2
return s:history_list()
endfunction
function! ku#make_path(...) "{{{2
if a:0 == 1 && type(a:1) is type([])
return join(a:1, s:PATH_SEP)
else
return join(a:000, s:PATH_SEP)
endif
endfunction
function! ku#path_separator() "{{{2
return s:PATH_SEP
endfunction
function! ku#restart() "{{{2
return ku#start(s:last_used_source, s:last_used_input_pattern)
endfunction
function! ku#set_the_current_input_pattern(s) "{{{2
if s:ku_active_p()
let old_one = s:remove_prompt(getline(s:LNUM_INPUT))
call setline(s:LNUM_INPUT, a:s)
return old_one
else
return 0
endif
endfunction
function! ku#start(source, ...) "{{{2
if !ku#available_source_p(a:source)
echoerr 'ku: Not a valid source name:' string(a:source)
return s:FALSE
endif
if s:ku_active_p()
echoerr 'ku: Already active - called with:'
\ string(a:source) 'and' string(a:000)
return s:FALSE
endif
let s:current_source = a:source
let s:session_id = localtime()
let s:current_hisotry_index = -1
" Save some values to restore the original state.
let s:original_completeopt = &completeopt
let s:original_ignorecase = &ignorecase
let s:original_curwinnr = winnr()
let s:original_winrestcmd = winrestcmd()
" Open or create the ku buffer.
let v:errmsg = ''
if bufexists(s:bufnr)
topleft split
if v:errmsg != ''
return s:FALSE
endif
silent execute s:bufnr 'buffer'
else
topleft new
if v:errmsg != ''
return s:FALSE
endif
let s:bufnr = bufnr('')
call s:initialize_ku_buffer()
endif
2 wincmd _
" Set some options
set completeopt=menu,menuone
set ignorecase
" Reset the content of the ku buffer
silent % delete _
call append(1, (a:0 == 0 ? '' : a:1))
normal! 2G
" Start Insert mode.
call feedkeys('A', 'n')
call s:api_on_source_enter(s:current_source)
return s:TRUE
endfunction
function! ku#switch_source(source) "{{{2
if !s:ku_active_p()
echoerr 'ku: Not active - called with:' string(a:source)
return s:FALSE
endif
if !ku#available_source_p(a:source)
echoerr 'ku: Unavailable source:' string(a:source)
return s:FALSE
endif
call s:switch_current_source(a:source)
return s:TRUE
endfunction
" Core "{{{1
" Completion "{{{2
" Variables on omnifunc "{{{3
let s:_OMNIFUNC_INVALID = [] " to indicate values not in the cache.
let s:_omnifunc_cache = {} " '{source}{prompt}{pattern}' => [item, ...]
let s:_omnifunc_session_id = 0 " to clear the cache for each ku session.
function! ku#_omnifunc(findstart, base) "{{{3
" items = a list of items
" item = a dictionary as described in :help complete-items.
" '^ku__.*$' - additional keys used by ku.
" '^ku_{source}_.*$' - additional keys used by {source}.
if a:findstart
let s:last_completed_items = []
if s:_omnifunc_session_id != s:session_id
" Clear the cache for each ku session.
let s:_omnifunc_cache = {}
let s:_omnifunc_session_id = s:session_id
endif
return 0
else
let pattern = s:expand_prefix(s:remove_prompt(a:base))
let cache_key = s:_omnifunc_cache_key(pattern)
let cached_value = get(s:_omnifunc_cache, cache_key, s:_OMNIFUNC_INVALID)
if cached_value is s:_OMNIFUNC_INVALID
if pattern == '' || s:api_special_char_p(s:current_source, pattern[-1:])
" Base cases.
let _ = s:_omnifunc_core(
\ s:current_source,
\ pattern,
\ s:api_gather_items(s:current_source, pattern)
\ )
else
" The last character (which seems to be typed by user)
" is not a special character i.e. ordinary character.
" Make a list of items
" by filtering a cache for a base case to the given pattern.
let _ = s:_omnifunc_core(
\ s:current_source,
\ pattern,
\ ku#_omnifunc(s:FALSE, s:_omnifunc_base_case_pattern(pattern))
\ )
endif
let cached_value = _
let s:_omnifunc_cache[cache_key] = _
else
endif
let s:last_completed_items = cached_value
return s:last_completed_items
endif
endfunction
function! ku#_omnifunc_profile(source, pattern, ...) "{{{3
let n = a:0 ? a:1 : 1
let base_time = reltime()
let raw_items = s:api_gather_items(a:source, a:pattern)
let gathering_time = reltime(base_time)
let base_time = reltime()
for _ in range(n)
let filtered_items = s:_omnifunc_core(a:source, a:pattern, raw_items)
endfor
let filtering_time = reltime(base_time)
return [reltimestr(gathering_time), reltimestr(filtering_time)]
endfunction
function! s:_omnifunc_core(current_source, pattern, items) "{{{3
" NB: This function doesn't know about the cache.
let INFINITY = 2147483647 " to easily sort by ku__sort_priorities.
" Prefix assumption - By automatic component completion, it's hard to insert
" text with uncompleted "prefix", so that "prefix" is excluded to match.
let i = match(a:pattern,
\ s:regexp_not_any_char_of(g:ku_component_separators) . '\*\$')
let prefix = i == 0 ? '' : a:pattern[:i-1]
let pattern = a:pattern[(i):]
let empty_pattern_p = pattern == ''
let asis_regexp = s:make_asis_regexp(pattern)
let word_regexp = s:make_word_regexp(pattern)
let skip_regexp = s:make_skip_regexp(pattern)
if empty_pattern_p
" Dummy values for ku__sort_priorities,
" because match()/matchend() are skipped for empty "pattern" for speed-up.
let asis_C_ms = 0
let asis_c_ms = 0
let skip_C_me = 0
let skip_C_ms = 0
let skip_c_me = 0
let skip_c_ms = 0
let word_C_me = 0
let word_C_ms = 0
let word_c_me = 0
let word_c_ms = 0
endif
let source_junk_pattern = (exists('g:ku_{a:current_source}_junk_pattern')
\ ? g:ku_{a:current_source}_junk_pattern
\ : 0)
let re_acc_sep = '\ze' . s:regexp_any_char_of(g:ku_component_separators)
let items = copy(a:items)
if 0 < i " ensure prefix assumption
call filter(items, 'v:val.word[:i-1] ==# prefix')
endif
for _ in items
let _['ku__completed_p'] = s:TRUE
let _['ku__source'] = a:current_source
if empty_pattern_p
" To skip unnecessary checkings in s:_omnifunc_compare_lists(),
" use the unique part of _.word which is matched to patterns.
let asis_C_ms = substitute(_.word[(i):], re_acc_sep, ' ', 'g')
let word = 0
else
" Skip many match()/matchend() callings by the following conditions:
" (a) If match() is failed for a pattern,
" it's not necessary to call matchend() for that pattern.
" (b) If a case-insensitive pattern is not matched,
" the corresponding case-sensitive pattern is not also matched.
" (c) If a "skip" pattern is not matched,
" the corresponding "word" pattern is not also matched.
" If a "word" pattern is not matched,
" the corresponding "asis" pattern is not also matched.
" Cases (c)
let skip_c_ms = match(_.word, '\c' . skip_regexp, i)
let word_c_ms = skip_c_ms < 0 ? -1 : match(_.word, '\c' . word_regexp, i)
let asis_c_ms = word_c_ms < 0 ? -1 : match(_.word, '\c' . asis_regexp, i)
" Cases (b)
let skip_C_ms = skip_c_ms < 0 ? -1 : match(_.word, '\C' . skip_regexp, i)
let word_C_ms = word_c_ms < 0 ? -1 : match(_.word, '\C' . word_regexp, i)
let asis_C_ms = asis_c_ms < 0 ? -1 : match(_.word, '\C' . asis_regexp, i)
" Cases (a)
let skip_c_me = skip_c_ms < 0 ? -1 : matchend(_.word,'\c'.skip_regexp, i)
let skip_C_me = skip_C_ms < 0 ? -1 : matchend(_.word,'\C'.skip_regexp, i)
let word_c_me = word_c_ms < 0 ? -1 : matchend(_.word,'\c'.word_regexp, i)
let word_C_me = word_C_ms < 0 ? -1 : matchend(_.word,'\C'.word_regexp, i)
let asis_C_ms = asis_C_ms < 0 ? INFINITY : asis_C_ms
let asis_c_ms = asis_c_ms < 0 ? INFINITY : asis_c_ms
let word_C_me = word_C_me < 0 ? INFINITY : word_C_me
let word_C_ms = word_C_ms < 0 ? INFINITY : word_C_ms
let word_c_me = word_c_me < 0 ? INFINITY : word_c_me
let word_c_ms = word_c_ms < 0 ? INFINITY : word_c_ms
let word = substitute(_.word[(i):], re_acc_sep, ' ', 'g')
endif
let _['ku__sort_priorities'] = [
\ get(_, 'ku__sort_priority', 0),
\ _.word =~# g:ku_common_junk_pattern,
\ source_junk_pattern is 0 ? 0 : _.word =~# source_junk_pattern,
\ asis_C_ms, asis_c_ms,
\ word_C_ms, word_C_me, word_c_ms, word_c_me,
\ skip_C_ms, skip_C_me, skip_c_ms, skip_c_me,
\ word,
\ ]
endfor
" Remove items not matched to case-insensitive skip_regexp, because user
" doesn't want such items to be completed.
" BUGS: Don't forget to update the index for the matched position of
" case-insensitive skip_regexp.
call filter(items, '0 <= v:val.ku__sort_priorities[-3]')
call sort(items, function('s:_omnifunc_compare_items'))
if exists('g:ku_debug_p') && g:ku_debug_p
echomsg 'pattern' string(a:pattern)
echomsg 'asis' string(asis_regexp)
echomsg 'word' string(word_regexp)
echomsg 'skip' string(skip_regexp)
for _ in items
echomsg string(_.ku__sort_priorities)
endfor
endif
return items
endfunction
function! s:_omnifunc_base_case_pattern(pattern) "{{{3
let i = len(a:pattern) - 1
while (0 <= i
\ && !s:api_special_char_p(s:current_source, a:pattern[i])
\ && !has_key(s:_omnifunc_cache, s:_omnifunc_cache_key(a:pattern[:i])))
let i -= 1
endwhile
return 0 <= i ? a:pattern[:i] : ''
endfunction
function! s:_omnifunc_cache_key(pattern) "{{{3
return s:current_source . s:PROMPT . a:pattern
endfunction
function! s:_omnifunc_compare_items(a, b) "{{{3
return s:_omnifunc_compare_lists(a:a.ku__sort_priorities,
\ a:b.ku__sort_priorities)
endfunction
function! s:_omnifunc_compare_lists(a, b) "{{{3
" Assumption: len(a:a) == len(a:b)
for i in range(len(a:a))
if a:a[i] < a:b[i]
return -1
elseif a:a[i] > a:b[i]
return 1
endif
endfor
return 0
endfunction
function! s:do(action_name) "{{{2
let current_user_input_raw = getline(s:LNUM_INPUT)
if current_user_input_raw !=# s:last_user_input_raw
" current_user_input_raw seems to be inserted by completion.
for _ in s:last_completed_items
if current_user_input_raw ==# _.word
let item = _
break
endif
endfor
if !exists('item')
echoerr 'Internal error: No match found in s:last_completed_items'
echoerr 'current_user_input_raw' string(current_user_input_raw)
echoerr 's:last_user_input_raw' string(s:last_user_input_raw)
echoerr 's:last_completed_items' string(s:last_completed_items)
throw 'ku:e1'
endif
else
" current_user_input_raw seems NOT to be inserted by completion, but ...
if 0 < len(s:last_completed_items)
" there are 1 or more items -- user seems to take action on the 1st one.
let item = s:last_completed_items[0]
else
" there's no item -- user seems to take action on current_user_input_raw.
let item = {'word':
\ s:expand_prefix(s:remove_prompt(current_user_input_raw)),
\ 'ku__completed_p': s:FALSE,
\ 'ku__source': s:current_source}
endif
endif
let nothing_to_do_p = item.word ==# '' && !(item.ku__completed_p)
if nothing_to_do_p
" Ignore.
elseif a:action_name ==# '*choose*' || a:action_name ==# '*persistent*'
let action = s:choose_action(item, a:action_name ==# '*persistent*')
else
let action = a:action_name
endif
" To avoid doing some actions on this buffer and/or this window, close the
" ku window.
call s:end()
if nothing_to_do_p
echo 'Nothing to do, because you gave empty pattern and there is no item.'
elseif action ==# 'cancel' || action ==# 'nop'
" Ignore.
elseif action ==# 'selection'
call ku#restart() " Emulate to return to the previous selection.
else
call s:history_add(s:remove_prompt(s:last_used_input_pattern),
\ s:last_used_source)
let item = s:api_on_before_action(s:current_source, item)
call s:do_action(action, item)
if a:action_name ==# '*persistent*'
call ku#restart()
endif
endif
return
endfunction
function! s:end() "{{{2
if s:_end_locked_p
return s:FALSE
endif
let s:_end_locked_p = s:TRUE
" Another choise is getline(s:LNUM_INPUT) (= the current input pattern in
" the ku buffer), but it is improper for the following reason:
"
" - Return value from getline(s:LNUM_INPUT) may be an item which was
" selected from the completion menu if s:last_user_input_raw and
" getline(s:LNUM_INPUT) are the same value.
" - Users don't want to continue a selection with such completed value by
" ku#start() and <Plug>(ku-do-persistent-action), because typical usage
" of them is to do some action for several items which are matched to
" a pattern.
"
" So here we have to use s:last_user_input_raw instead.
let s:last_used_input_pattern = s:last_user_input_raw
let s:last_used_source = s:current_source
call s:api_on_source_leave(s:current_source)
close
let &completeopt = s:original_completeopt
let &ignorecase = s:original_ignorecase
execute s:original_curwinnr 'wincmd w'
execute s:original_winrestcmd
let s:_end_locked_p = s:FALSE
return s:TRUE
endfunction
let s:_end_locked_p = s:FALSE
function! s:initialize_ku_buffer() "{{{2
" Basic settings.
setlocal bufhidden=hide
setlocal buftype=nofile
setlocal nobuflisted
setlocal noswapfile
setlocal omnifunc=ku#_omnifunc
silent file `=g:ku_buffer_name`
" Autocommands.
autocmd InsertEnter <buffer> call feedkeys(s:on_InsertEnter(), 'n')
autocmd CursorMovedI <buffer> call feedkeys(s:on_CursorMovedI(), 'n')
autocmd BufLeave <buffer> call s:end()
autocmd WinLeave <buffer> call s:end()
" autocmd TabLeave <buffer> call s:end() " not necessary
" Key mappings.
nnoremap <buffer> <silent> <Plug>(ku-cancel)
\ :<C-u>call <SID>end()<Return>
nnoremap <buffer> <silent> <Plug>(ku-do-the-default-action)
\ :<C-u>call <SID>do('default')<Return>
nnoremap <buffer> <silent> <Plug>(ku-choose-an-action)
\ :<C-u>call <SID>do('*choose*')<Return>
nnoremap <buffer> <silent> <Plug>(ku-do-persistent-action)
\ :<C-u>call <SID>do('*persistent*')<Return>
nnoremap <buffer> <silent> <Plug>(ku-next-source)
\ :<C-u>call <SID>switch_current_source(1)<Return>
nnoremap <buffer> <silent> <Plug>(ku-previous-source)
\ :<C-u>call <SID>switch_current_source(-1)<Return>
nnoremap <buffer> <silent> <Plug>(ku-choose-source)
\ :<C-u>call <SID>switch_current_source('source')<Return>
nnoremap <buffer> <silent> <Plug>(ku-history-source)
\ :<C-u>call <SID>switch_current_source('*history*')<Return>
nnoremap <buffer> <silent> <Plug>(ku-newer-history)
\ :<C-u>call <SID>recall_input_history(-1, 0)<Return>
nnoremap <buffer> <silent> <Plug>(ku-older-history)
\ :<C-u>call <SID>recall_input_history(1, 0)<Return>
nnoremap <buffer> <silent> <Plug>(ku-newer-history-and-source)
\ :<C-u>call <SID>recall_input_history(-1, !0)<Return>
nnoremap <buffer> <silent> <Plug>(ku-older-history-and-source)
\ :<C-u>call <SID>recall_input_history(1, !0)<Return>
nnoremap <buffer> <Plug>(ku-%-enter-insert-mode) a
inoremap <buffer> <Plug>(ku-%-leave-insert-mode) <Esc>
inoremap <buffer> <expr> <Plug>(ku-%-accept-completion)
\ pumvisible() ? '<C-y>' : ''
inoremap <buffer> <expr> <Plug>(ku-%-cancel-completion)
\ pumvisible() ? '<C-e>' : ''
imap <buffer> <silent> <Plug>(ku-cancel)
\ <Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-cancel)
imap <buffer> <silent> <Plug>(ku-do-the-default-action)
\ <Plug>(ku-%-accept-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-do-the-default-action)
imap <buffer> <silent> <Plug>(ku-choose-an-action)
\ <Plug>(ku-%-accept-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-choose-an-action)
imap <buffer> <silent> <Plug>(ku-do-persistent-action)
\ <Plug>(ku-%-accept-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-do-persistent-action)
imap <buffer> <silent> <Plug>(ku-next-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-next-source)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-previous-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-previous-source)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-choose-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-choose-source)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-history-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-history-source)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-newer-history)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-newer-history)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-older-history)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-older-history)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-newer-history-and-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-newer-history-and-source)
\<Plug>(ku-%-enter-insert-mode)
imap <buffer> <silent> <Plug>(ku-older-history-and-source)
\ <Plug>(ku-%-cancel-completion)
\<Plug>(ku-%-leave-insert-mode)
\<Plug>(ku-older-history-and-source)
\<Plug>(ku-%-enter-insert-mode)
inoremap <buffer> <expr> <BS> pumvisible() ? '<C-e><BS>' : '<BS>'
imap <buffer> <C-h> <BS>
" <C-n>/<C-p> ... Vim doesn't expand these keys in Insert mode completion.
" User's initialization.
setfiletype ku
if !(exists('#FileType#ku') || exists('b:did_ftplugin'))
call ku#default_key_mappings(s:FALSE)
endif
return
endfunction
function! s:on_CursorMovedI() "{{{2
" Calling setline() has a side effect to the cursor. If the cursor beyond
" the end of line (i.e. getline('.') < col('.')), the cursor will be move at
" the last character of the current line after calling setline().
let c0 = col('.')
call setline(s:LNUM_STATUS, '')
let c1 = col('.')
if s:current_hisotry_index == -1
call setline(s:LNUM_STATUS, printf('Source: %s', s:current_source))
else
let old_source = ku#input_history()[s:current_hisotry_index].source
if s:current_source ==# old_source
let _ = ''
else
let _ = printf(' (was %s)', old_source)
endif
call setline(s:LNUM_STATUS,
\ printf('Source: %s (%d/%d)%s',
\ s:current_source,
\ s:current_hisotry_index + 1,
\ len(ku#input_history()),
\ _))
endif
" The order of these conditions are important.
let line = getline('.')
if !s:contains_the_prompt_p(line)
" Complete the prompt if it doesn't exist for some reasons.
let keys = repeat("\<Right>", len(s:PROMPT))
call s:complete_the_prompt()
elseif col('.') <= len(s:PROMPT)
" Move the cursor out of the prompt if it is in the prompt.
let keys = repeat("\<Right>", len(s:PROMPT) - col('.') + 1)
elseif len(line) < col('.') && col('.') != s:last_col
" New character is inserted. Let's complete automatically.
if (!s:automatic_component_completion_done_p)
\ && 0 <= stridx(g:ku_component_separators, line[-1:])
\ && len(s:PROMPT) + 2 <= len(line)
" (1) The last inserted character is not inserted by automatic component
" completion.
" (2) It is a special character which is one of
" g:ku_component_separators.
" (3) It seems not to be the 1st one in line.
"
" The (3) is necessary to input a special character as the 1st character
" in line. For example, without this condition, user cannot input the
" 1st '/' of an absolute path like '/usr/local/bin' if '/' is a special
" character.
let text = s:text_by_automatic_component_completion(line)
if text != ''
call setline('.', text)
" The last special character must be inserted in this way to
" forcedly show the completion menu.
let keys = "\<End>" . line[-1:]
let s:automatic_component_completion_done_p = s:TRUE
else
" Delete the last inserted character to express that ACC is failed.
let keys = (g:ku_acc_style =~# '\<rejection\>'
\ ? "\<C-h>"
\ : s:KEYS_TO_START_COMPLETION)
let s:automatic_component_completion_done_p = s:FALSE
endif
else
let keys = s:KEYS_TO_START_COMPLETION
let s:automatic_component_completion_done_p = s:FALSE
endif
else
let keys = ''
endif
let s:last_col = col('.')
let s:last_user_input_raw = line
return (c0 != c1 ? "\<Right>" : '') . keys
endfunction
function! s:on_InsertEnter() "{{{2
let s:last_col = s:INVALID_COL
let s:last_user_input_raw = ''
let s:automatic_component_completion_done_p = s:FALSE
return s:on_CursorMovedI()
endfunction
function! s:recall_input_history(delta, change_source_p) "{{{2
let o = s:current_hisotry_index
let n = o + a:delta
if n < -1
let n = -1
endif
if len(ku#input_history()) <= n
let n = len(ku#input_history()) - 1
endif
if o == -1
let s:unsaved_input_pattern = getline('.')
endif
if n == -1
let _ = s:unsaved_input_pattern
else
let _ = ku#input_history()[n].pattern
if a:change_source_p
let new_source = ku#input_history()[n].source
if ku#available_source_p(new_source)
call s:switch_current_source(new_source)
endif
endif
endif
let s:current_hisotry_index = n
call setline('.', _)
call feedkeys("\<End>", 'n')
return
endfunction
" s:unsaved_input_pattern = ''
function! s:switch_current_source(new_source) "{{{2
" a:new_source must be:
" - A number - offset based on the current source in :help ku-sources-list.
" - A string - a valid source name or '*history*'.
" '*history*' is treated as the source name for the currently
" recalled input pattern from :help ku-input-history.
"
" FIXME: Update the line to indicate the current source even if this
" function is called in any mode other than Insert mode.
let _ = ku#available_sources()
let o = index(_, s:current_source)
if type(a:new_source) == type(0)
let n = (o + a:new_source) % len(_)
if n < 0
let n += len(_)
endif
else " type(a:new_source) == type('')
if a:new_source ==# '*history*'
if 0 <= s:current_hisotry_index
let new_source = ku#input_history()[s:current_hisotry_index].source
if !ku#available_source_p(new_source)
return s:FALSE
endif
else
return s:FALSE
endif
else
let new_source = a:new_source
endif
let n = index(_, new_source)
endif
if o == n
return s:FALSE
endif
call s:api_on_source_leave(_[o])
call s:api_on_source_enter(_[n])
let s:current_source = _[n]
return s:TRUE
endfunction
" Misc. "{{{1
" Autocommands "{{{2
augroup plugin-ku
autocmd!
autocmd CursorHold *
\ if (g:ku_history_reloading_style ==# 'idle'
\ || g:ku_history_reloading_style ==# 'each')
\ | call s:history_reload()
\ | let s:after_idle_p = s:TRUE
\ | endif
autocmd CursorHoldI * doautocmd plugin-ku CursorHold
autocmd VimLeave * call s:history_save()
augroup END
" Automatic completion "{{{2
function! s:complete_the_prompt() "{{{3
call setline('.', s:PROMPT . getline('.'))
return
endfunction
function! s:contains_the_prompt_p(s) "{{{3
return len(s:PROMPT) <= len(a:s) && a:s[:len(s:PROMPT) - 1] ==# s:PROMPT
endfunction
function! s:remove_prompt(s) "{{{3
return s:contains_the_prompt_p(a:s) ? a:s[len(s:PROMPT):] : a:s
endfunction
function! s:text_by_automatic_component_completion(line) "{{{3
" Note that a:line always ends with a special character which is one of
" g:ku_component_separators, because this function is always called by
" typing a special character. So there are at least 2 components in a:line.
let SEP = a:line[-1:] " string[-1] is always empty - see :help expr-[]
let user_input_raw = s:remove_prompt(a:line)
let [user_input_ped, prefix, text] = s:expand_prefix3(user_input_raw)
let prefix_expanded_p = user_input_raw !=# user_input_ped
let line_components = split(user_input_ped, SEP, s:TRUE)
" Find an item which has the same components but the last 2 ones of
" line_components. Because line_components[-1] is always empty and
" line_components[-2] is almost imperfect name of a component.
"
" Note that line_components[-2] is already used to filter the completed
" items and it is used to select what components should be completed.
"
" Example:
"
" (a) a:line ==# 'usr/share/m/',
" line_components == ['usr', 'share', 'm', '']
"
" The 1st item which is prefixed with 'usr/share/' is selected and it is
" used for this automatic component completion. If
" 'usr/share/man/man1/' is found in this way, the completed text will be
" 'usr/share/man'.
"
" (b) a:line ==# 'u/'
" line_components == ['u', '']
"
" The 1st item is alaways selected for this automatic component
" completion. If 'usr/share/man/man1/' is found in this way, the
" completion text will be 'usr'.
"
" (c) a:line ==# 'm/'
" line_components == ['m', '']
"
" The 1st item is alaways selected for this automatic component
" completion. If 'usr/share/man/man1/' is found in this way, the
" completion text will be 'usr/share/man', because user seems to want to
" complete till the component which matches to 'm'.
let items = copy(ku#_omnifunc(s:FALSE, a:line[:-2])) " without the last SEP
for item in items
let item_components = split(item.word, SEP, s:TRUE)
if len(line_components) < 2
echoerr 'Assumption is failed in auto component completion'
throw 'ku:e2'
elseif len(line_components) == 2
" OK - the case (b)
elseif len(line_components) - 2 <= len(item_components)
for i in range(len(line_components) - 2)
if line_components[i] != item_components[i]
break
endif
endfor
if line_components[i] != item_components[i]
continue
endif
" OK - the case (a)
else
continue
endif
" for the case (c), find the index of a component to be completed.
let _ = len(line_components) - 2
let p = '\c' . s:make_skip_regexp(line_components[-2])
let t = join(item_components[_ :], SEP) " p may be matched many components
let i = matchend(t, p)
" echomsg 'line' string(a:line)
" echomsg 'item.word' string(item.word)
" echomsg '_' string(_)
" echomsg 'p' string(p)
" echomsg 't' string(t)
" echomsg 'i' string(i)
if 0 <= i
let j = stridx(t, SEP, i)
" echomsg 'j' string(j)
if 0 <= j
let result = item.word[:-(len(t)-j+1)]
elseif s:api_acc_valid_p(s:current_source, item, SEP)
" echomsg 'acc_valid_p'
let result = join(item_components[:_], SEP)
else
" echomsg '(c) failed'
continue
endif
else
" By 'omnifunc', the last pattern matching must be succeeded.
echoerr 'Assumption is failed in auto component completion'
throw 'ku:e3'
endif
if prefix_expanded_p && stridx(result, text) == 0
let result = prefix . result[len(text):]
endif
return result
endfor
return ''
endfunction
" Action-related stuffs "{{{2
function! s:choose_action(item, persistent_p) "{{{3
" Prompt Item Source
" | | |
" _^__ _^______ _^__
" Item: Makefile (file)
" ^C cancel ^O open ...
" What action? ~~ ~~~~
" ~~~~~~~~~~~~ | |
" | | |
" Message Key Action
"
" Here "Prompt" is highlighted with kuChoosePrompt,
" "Item" is highlighted with kuChooseItem, and so forth.
let KEY_TABLE = s:composite_key_table(s:current_source)
call filter(KEY_TABLE, 'v:val !=# "nop"')
let ACTION_TABLE = s:composite_action_table(s:current_source)
call filter(KEY_TABLE, 'get(ACTION_TABLE, v:val, "") !=# "nop"')
" "Item: {item} ({source})"
echohl NONE
echo ''
echohl kuChoosePrompt
echon 'Item'
echohl NONE
echon ': '
echohl kuChooseItem
echon a:item.word
echohl NONE
echon ' ('
echohl kuChooseSource
echon s:current_source
echohl NONE
echon ')'
if g:ku_choosing_actions_sorting_style ==# 'by-key'
call s:list_actions_sorted_by_key(KEY_TABLE)
else
call s:list_actions_sorted_by_action(KEY_TABLE)
endif
echohl kuChooseMessage
echo 'What action?' (a:persistent_p ? '[persistent]' : '')
echohl NONE
" Take user input.
let k = s:getkey()
redraw " clear the menu message lines to avoid hit-enter prompt.
" Return the action bound to the key k.
if has_key(KEY_TABLE, k)
return KEY_TABLE[k]
else
" FIXME: loop to rechoose?
echo 'The key' string(k) 'is not associated with any action'
\ '-- nothing happened.'
return 'nop'
endif
endfunction
function! s:do_action(action, item, ...) "{{{3
" Assumption: BeforeAction is already applied for a:item.
let composite_p = 1 <= a:0 ? a:1 : s:TRUE
let _ = function(s:get_action_function(a:action, composite_p))(a:item)
if _ isnot 0
echohl ErrorMsg
echomsg _
echohl NONE
endif
return _
endfunction
function! s:get_action_function(action, composite_p) "{{{3
let ACTION_TABLE = (a:composite_p
\ ? s:composite_action_table(s:current_source)
\ : s:api_action_table(s:current_source))
if has_key(ACTION_TABLE, a:action) " exists action?
if ACTION_TABLE[a:action] !=# 'nop' " enabled action?
return ACTION_TABLE[a:action]
else
break
endif
endif
" To avoid echoing the location of error,
" use :echohl ErrorMsg instead of :echoerr.
echohl ErrorMsg
echo printf('No such action for source %s: %s',
\ string(s:current_source),
\ string(a:action))
echohl NONE
return 's:_default_action_nop'
endfunction
function! s:list_actions_sorted_by_action(KEY_TABLE) "{{{3
let ACTION_TABLE = {}
for [key, action] in items(a:KEY_TABLE)
if !has_key(ACTION_TABLE, action)
let ACTION_TABLE[action] = {'keys': []}
endif
call add(ACTION_TABLE[action].keys, [key, strtrans(key)])
endfor
for _ in values(ACTION_TABLE)
call sort(_.keys)
let _.label = join(map(copy(_.keys), 'v:val[1]'), ' ')
endfor
let ACTION_NAMES = sort(keys(ACTION_TABLE), 's:compare_ignorecase')
let MAX_ACTION_NAME_WIDTH = max(map(keys(ACTION_TABLE), 'len(v:val)'))
let MAX_LABEL_WIDTH = max(map(values(ACTION_TABLE), 'len(v:val.label)'))
let MAX_CELL_WIDTH = MAX_ACTION_NAME_WIDTH + 1 + MAX_LABEL_WIDTH
let SPACER = ' '
let C = (&columns + len(SPACER) - 1) / (MAX_CELL_WIDTH + len(SPACER))
let C = max([C, 1])
let N = len(ACTION_TABLE)
let R = N / C + (N % C != 0)
unlet _
for row in range(R)
for col in range(C)
let i = col * R + row
if !(i < N)
continue
endif
echon col == 0 ? "\n" : SPACER
echohl kuChooseAction
let _ = ACTION_NAMES[i]
echon _
echohl NONE
echon repeat(' ', MAX_ACTION_NAME_WIDTH - len(_))
echohl kuChooseKey
echon ' '
let _ = ACTION_TABLE[ACTION_NAMES[i]].label
echon _
echohl NONE
echon repeat(' ', MAX_LABEL_WIDTH - len(_))
endfor
endfor
endfunction
function! s:list_actions_sorted_by_key(KEY_TABLE) "{{{3
let KEYS = map(sort(keys(a:KEY_TABLE)), 'v:val')
let KEY_NAMES = map(copy(KEYS), 'strtrans(v:val)')
let MAX_KEY_WIDTH = max(map(copy(KEY_NAMES), 'len(v:val)'))
let ACTION_NAMES = map(copy(KEYS), 'a:KEY_TABLE[v:val]')
let MAX_ACTION_WIDTH = max(map(copy(ACTION_NAMES), 'len(v:val)'))
let MAX_LABEL_WIDTH = MAX_KEY_WIDTH + 1 + MAX_ACTION_WIDTH
let SPACER = ' '
let C = (&columns + len(SPACER) - 1) / (MAX_LABEL_WIDTH + len(SPACER))
let C = max([C, 1])
let N = len(a:KEY_TABLE)
let R = N / C + (N % C != 0)
for row in range(R)
for col in range(C)
let i = col * R + row
if !(i < N)
continue
endif
echon col == 0 ? "\n" : SPACER
echohl kuChooseKey
echon KEY_NAMES[i]
echohl NONE
echon repeat(' ', MAX_KEY_WIDTH - len(KEY_NAMES[i]))
echon ' '
echohl kuChooseAction
echon ACTION_NAMES[i]
echohl NONE
echon repeat(' ', MAX_ACTION_WIDTH - len(ACTION_NAMES[i]))
endfor
endfor
endfunction
" Default actions "{{{2
" "default" variants with :split "{{{3
function! s:with_split(direction_modifier, item)
let original_tabpagenr = tabpagenr()
let original_curwinnr = winnr()
let original_winrestcmd = winrestcmd()
let v:errmsg = ''
silent! execute a:direction_modifier 'split'
if v:errmsg != ''
return v:errmsg
endif
" Here we have to do "default" action of the default action table for the
" current source instead of composite action table - because the latter
" may cause infinitely recursive loop if "default" action is overriden by
" other action which refers "default" action, such as "tab-Right".
let _ = s:do_action('default', a:item, s:FALSE)
if _ isnot 0
" Undo the last :split.
close
execute 'tabnext' original_tabpagenr
execute original_curwinnr 'wincmd w'
execute original_winrestcmd
endif
return _
endfunction
function! s:_default_action_Bottom(item)
return s:with_split('botright', a:item)
endfunction
function! s:_default_action_Left(item)
return s:with_split('vertical topleft', a:item)
endfunction
function! s:_default_action_Right(item)
return s:with_split('vertical botright', a:item)
endfunction
function! s:_default_action_Top(item)
return s:with_split('topleft', a:item)
endfunction
function! s:_default_action_above(item)
return s:with_split('aboveleft', a:item)
endfunction
function! s:_default_action_below(item)
return s:with_split('belowright', a:item)
endfunction
function! s:_default_action_left(item)
return s:with_split('vertical leftabove', a:item)
endfunction
function! s:_default_action_right(item)
return s:with_split('vertical rightbelow', a:item)
endfunction
function! s:_default_action_tab_Left(item)
return s:with_split('0 tab', a:item)
endfunction
function! s:_default_action_tab_Right(item)
return s:with_split(tabpagenr('$') . ' tab', a:item)
endfunction
function! s:_default_action_tab_left(item)
return s:with_split((tabpagenr() - 1) . ' tab', a:item)
endfunction
function! s:_default_action_tab_right(item)
return s:with_split('tab', a:item)
endfunction
function! s:_default_action_cd(item) "{{{3
let v:errmsg = ''
silent! cd `=fnamemodify(a:item.word, ':p:h')`
return v:errmsg == '' ? 0 : v:errmsg
endfunction
function! s:_default_action_default(item) "{{{3
return 'ku: Source ' . string(a:item.ku__source)
\ . ' does not have the "default" action'
endfunction
function! s:_default_action_ex(item) "{{{3
" Support to execute an Ex command on a:item.word (as path).
call feedkeys(printf(": %s\<C-b>", fnameescape(a:item.word)), 'n')
return 0
endfunction
function! s:_default_action_lcd(item) "{{{3
let v:errmsg = ''
silent! lcd `=fnamemodify(a:item.word, ':p:h')`
return v:errmsg == '' ? 0 : v:errmsg
endfunction
function! s:_default_action_nop(item) "{{{3
" NOP
return 0
endfunction
" Action table "{{{2
function! s:composite_action_table(source) "{{{3
let action_table = {}
for _ in [s:default_action_table(),
\ s:custom_action_table('common'),
\ s:api_action_table(a:source),
\ s:custom_action_table(a:source)]
call extend(action_table, _)
endfor
return action_table
endfunction
function! s:custom_action_table(source) "{{{3
return get(s:custom_action_tables, a:source, {})
endfunction
function! s:default_action_table() "{{{3
return {
\ 'Bottom': 's:_default_action_Bottom',
\ 'Left': 's:_default_action_Left',
\ 'Right': 's:_default_action_Right',
\ 'Top': 's:_default_action_Top',
\ 'above': 's:_default_action_above',
\ 'below': 's:_default_action_below',
\ 'cancel': '*pseudo-action*',
\ 'cd': 's:_default_action_cd',
\ 'default': 's:_default_action_default',
\ 'ex': 's:_default_action_ex',
\ 'lcd': 's:_default_action_lcd',
\ 'left': 's:_default_action_left',
\ 'nop': '*pseudo-action*',
\ 'right': 's:_default_action_right',
\ 'selection': '*pseudo-action*',
\ 'tab-Left': 's:_default_action_tab_Left',
\ 'tab-Right': 's:_default_action_tab_Right',
\ 'tab-left': 's:_default_action_tab_left',
\ 'tab-right': 's:_default_action_tab_right',
\ }
endfunction
" Key table "{{{2
function! s:composite_key_table(source) "{{{3
let key_table = {}
for _ in [s:default_key_table(),
\ s:custom_key_table('common'),
\ s:api_key_table(a:source),
\ s:custom_key_table(a:source)]
call extend(key_table, _)
endfor
return key_table
endfunction
function! s:custom_key_table(source) "{{{3
return get(s:custom_key_tables, a:source, {})
endfunction
function! s:default_key_table() "{{{3
return {
\ "\<C-c>": 'cancel',
\ "\<C-h>": 'left',
\ "\<C-j>": 'below',
\ "\<C-k>": 'above',
\ "\<C-l>": 'right',
\ "\<C-r>": 'selection',
\ "\<C-t>": 'tab-Right',
\ "\<Esc>": 'cancel',
\ "\<Return>": 'default',
\ '/': 'cd',
\ ':': 'ex',
\ ';': 'ex',
\ '?': 'lcd',
\ 'H': 'Left',
\ 'J': 'Bottom',
\ 'K': 'Top',
\ 'L': 'Right',
\ 'h': 'left',
\ 'j': 'below',
\ 'k': 'above',
\ 'l': 'right',
\ 't': 'tab-Right',
\ }
endfunction
" Prefix table "{{{2
function! s:prefix_table_for(source) "{{{3
let PREFIX_TABLE = {}
for _ in [s:custom_prefix_table('common'),
\ s:custom_prefix_table(a:source)]
call extend(PREFIX_TABLE, _)
endfor
return PREFIX_TABLE
endfunction
function! s:custom_prefix_table(source) "{{{3
return get(s:custom_prefix_tables, a:source, {})
endfunction
function! s:expand_prefix(user_input_raw) "{{{3
return s:expand_prefix3(a:user_input_raw)[0]
endfunction
function! s:expand_prefix3(user_input_raw) "{{{3
if s:session_id != s:_session_id_expand_prefix3
\ || s:current_source != s:_current_source_expand_prefix3
let _ = s:prefix_table_for(s:current_source)
let s:cached_prefix_items = map(reverse(sort(keys(_))), '[v:val,_[v:val]]')
endif
for [prefix, text] in s:cached_prefix_items
if a:user_input_raw[:len(prefix) - 1] ==# prefix
return [text . a:user_input_raw[len(prefix):], prefix, text]
endif
endfor
return [a:user_input_raw, '', '']
endfunction
let s:cached_prefix_items = []
let s:_session_id_expand_prefix3 = 0
let s:_current_source_expand_prefix3 = s:INVALID_SOURCE
" History of inputted patterns "{{{2
" Variables / Constants "{{{3
let s:after_idle_p = s:FALSE " to reload the history file after idle.
" s:history_changed_p = s:FALSE
" s:history_file_mtime = 0 " the last modified time of the history file.
" s:inputted_patterns = [] " the first item is the newest inputted pattern.
let s:HISTORY_FILE = ku#make_path('info', 'ku', 'history')
" The format of history file is:
" - Each line is corresponding to an inputted pattern.
" - The first line is corresponding to the newest inputted pattern.
" - Each line consists of 3 columns separated by a tab;
" the 1st column is an inputted pattern,
" the 2nd column is the source for which the pattern was inputted, and
" the 3rd column is the time when the pattenr was inputted (in localtime()).
function! s:history_add(new_input_pattern, source) "{{{3
if !{g:ku_history_added_p}(a:new_input_pattern, a:source)
return
endif
call insert(s:inputted_patterns,
\ {'pattern': a:new_input_pattern,
\ 'source': a:source,
\ 'time': localtime()},
\ 0)
if g:ku_history_size < len(s:inputted_patterns)
unlet s:inputted_patterns[(g:ku_history_size):]
endif
let s:history_changed_p = s:TRUE
endfunction
function! ku#_history_added_p(new_input_pattern, source)
return (a:source !=# 'history'
\ && a:source != 'source'
\ && a:new_input_pattern !~ '^\s*$')
endfunction
function! s:history_file() "{{{3
return split(&runtimepath, ',')[0] . s:PATH_SEP . s:HISTORY_FILE
endfunction
function! s:history_list() "{{{3
if (g:ku_history_reloading_style ==# 'each'
\ || (g:ku_history_reloading_style ==# 'idle' && s:after_idle_p))
call s:history_reload()
let s:after_idle_p = s:FALSE
endif
return s:inputted_patterns
endfunction
function! s:history_load() "{{{3
let _ = []
if filereadable(s:history_file())
for line in readfile(s:history_file(), '', g:ku_history_size)
let columns = split(line, '\t')
call add(_, {
\ 'pattern': columns[0],
\ 'source': 2 <= len(columns) ? columns[1] : s:INVALID_SOURCE,
\ 'time': 3 <= len(columns) ? str2nr(columns[2]) : 0,
\ })
endfor
endif
return _
endfunction
if !exists('s:inputted_patterns')
let s:inputted_patterns = s:history_load()
let s:history_file_mtime = getftime(s:history_file())
let s:history_changed_p = s:FALSE
endif
function! s:history_reload() "{{{3
let file = s:history_file()
let mtime = getftime(file)
if mtime == -1 " history file is not found
let s:history_changed_p = s:TRUE
elseif mtime != s:history_file_mtime " history file is updated
let current_history = s:inputted_patterns
let new_history = s:history_load()
let s:inputted_patterns = s:merge_histories(current_history, new_history)
let s:history_changed_p = s:TRUE
else
" history file is not changed
endif
if s:history_changed_p
call s:history_save()
let s:history_file_mtime = getftime(file)
let s:history_changed_p = s:FALSE
endif
return
endfunction
function! s:history_save() "{{{3
let file = s:history_file()
let directory = fnamemodify(file, ':h')
if !isdirectory(directory)
call mkdir(directory, 'p')
endif
call writefile(map(copy(s:inputted_patterns),
\ 'v:val.pattern ."\t". v:val.source ."\t". v:val.time'),
\ file)
endfunction
" Source API wrappers "{{{2
function! s:api_acc_valid_p(source_name, item, separator) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
let _ = 'ku#'.source_name_base.'#acc_valid_p'
if exists('*{_}')
return {_}(source_name_ext, a:item, a:separator)
else
return s:FALSE
endif
endfunction
function! s:api_action_table(source_name) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
return ku#{source_name_base}#action_table(source_name_ext)
endfunction
function! s:api_available_sources(source_name_base) "{{{3
" Unlike other Source API functions,
" - This function takes source_name_base instead of source_name.
" - Sources must define this API.
return ku#{a:source_name_base}#available_sources()
endfunction
function! s:api_gather_items(source_name, pattern) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
return ku#{source_name_base}#gather_items(source_name_ext, a:pattern)
endfunction
function! s:api_key_table(source_name) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
return ku#{source_name_base}#key_table(source_name_ext)
endfunction
function! s:api_on_before_action(source_name, item) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
let _ = 'ku#'.source_name_base.'#on_before_action'
if exists('*{_}')
return {_}(source_name_ext, a:item)
else
return a:item
endif
endfunction
function! s:api_on_source_enter(source_name) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
let _ = 'ku#'.source_name_base.'#on_source_enter'
if exists('*{_}')
call {_}(source_name_ext)
endif
endfunction
function! s:api_on_source_leave(source_name) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
let _ = 'ku#'.source_name_base.'#on_source_leave'
if exists('*{_}')
call {_}(source_name_ext)
endif
endfunction
function! s:api_special_char_p(source_name, character) "{{{3
let [source_name_base, source_name_ext] = s:split_source_name(a:source_name)
let _ = 'ku#'.source_name_base.'#special_char_p'
if exists('*{_}')
return {_}(source_name_ext, a:character)
else
return 0 <= stridx(g:ku_component_separators, a:character)
endif
endfunction
" For tests "{{{2
function! ku#_local_variables()
return s:
endfunction
function! ku#_sid()
return matchstr(expand('<sfile>'), '^<SNR>\d\+_')
endfunction
function! s:compare_ignorecase(x, y) "{{{2
" Comparing function for sort() to do consistently case-insensitive sort.
"
" Because sort(list, 1) does case-insensitive sort,
" but its result may not be in a consistent order.
" For example,
" sort(['b', 'a', 'B', 'A'], 1) may return ['a', 'A', 'b', 'B'],
" sort(['b', 'A', 'B', 'a'], 1) may return ['A', 'a', 'b', 'B'],
" and so forth.
"
" With this function, sort() always return ['A', 'a', 'B', 'b'].
return a:x <? a:y ? -1
\ : (a:x >? a:y ? 1
\ : (a:x <# a:y ? -1
\ : (a:x ># a:y ? 1
\ : 0)))
endfunction
function! s:getkey() "{{{2
" Alternative getchar() to get a logical key such as <F1> and <M-{x}>.
let k = ''
let c = getchar()
while s:TRUE
let k .= type(c) == type(0) ? nr2char(c) : c
let c = getchar(0)
if c is 0
break
endif
endwhile
return k
endfunction
function! s:ku_active_p() "{{{2
return bufexists(s:bufnr) && bufwinnr(s:bufnr) != -1
endfunction
function! s:make_asis_regexp(s) "{{{2
return '\V' . escape(a:s, '\')
endfunction
function! s:make_skip_regexp(s) "{{{2
let _ = a:s
let _ = substitute(_, '\s\+', '', 'g')
let _ = escape(_, '\')
let _ = substitute(_[:-2], '[^\\]\zs', '\\.\\{-}', 'g') . _[-1:]
return '\V' . _
endfunction
function! s:make_word_regexp(s) "{{{2
let p_asis = s:make_asis_regexp(a:s)
return substitute(p_asis, '\s\+', '\\.\\{-}', 'g')
endfunction
function! s:merge_histories(a, b) "{{{2
" cat
let _ = a:a + a:b
" sort
call sort(_, 's:_compare_by_time')
call reverse(_)
" uniq
let i = 0
while i < len(_)
while i + 1 < len(_) && _[i] ==# _[i+1]
call remove(_, i)
let i += 1
endwhile
let i += 1
endwhile
return _
endfunction
function! s:_compare_by_time(a, b)
return a:a.time < a:b.time ? -1 : (a:a.time > a:b.time ? 1 : 0)
endfunction
function! s:ni_map(...) "{{{2
for _ in ['n', 'i']
silent! execute _.'map' join(a:000)
endfor
return
endfunction
function! s:regexp_any_char_of(cs) "{{{2
return '\V\[' . escape(a:cs, '\[]') . ']'
endfunction
function! s:regexp_not_any_char_of(cs) "{{{2
return '\V\[^' . escape(a:cs, '\[]^') . ']'
endfunction
function! s:runtime_files(glob_pattern) "{{{2
return split(globpath(&runtimepath, a:glob_pattern), '\n')
endfunction
function! s:sort_sources(_) "{{{2
let _ = a:_
let _ = map(_, 'get(s:priority_table, v:val, s:DEFAULT_PRIORITY) . v:val')
let _ = sort(_)
let _ = map(_, 'v:val[3:]') " Assumption: priority is 3-digit integer.
return _
endfunction
function! s:split_source_name(source_name) "{{{2
" ==> [source_name_base, source_name_ext]
return split(a:source_name.'/', '/', s:TRUE)[:1]
endfunction
" __END__ "{{{1
" vim: foldmethod=marker
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment