"=============================================================================
" File : autoload/unite/source/outline.vim
" Author : h1mesuke <himesuke@gmail.com>
" Updated : 2012-01-11
" Version : 0.5.1
" 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.
" }}}
"=============================================================================
let s:save_cpo = &cpo
set cpo&vim
"-----------------------------------------------------------------------------
" Constants
let s:OUTLINE_INFO_PATH = [
\ 'autoload/outline/',
\ 'autoload/unite/sources/outline/',
\ 'autoload/unite/sources/outline/defaults/',
\ ]
" NOTE: source <- [aliases...]
let s:OUTLINE_ALIASES = {
\ 'markdown': ['mkd', 'pandoc'],
\ 'cpp' : ['c'],
\ 'dosini' : ['cfg'],
\ 'tex' : ['plaintex'],
\ 'conf' : ['snippet'],
\ 'html' : ['eruby', 'xhtml'],
\ 'sh' : ['zsh'],
\ }
let s:OUTLINE_CACHE_DIR = unite#get_data_directory() . '/outline'
let s:supported_arguments = [ 'filetype', 'folding', 'update' ]
" Rename the cache directory if its name is still old, dotted style name.
" See http://d.hatena.ne.jp/tyru/20110824/unite_file_mru
"
let s:old_cache_dir = unite#get_data_directory() . '/.outline'
if isdirectory(s:OUTLINE_CACHE_DIR)
if isdirectory(s:old_cache_dir)
call unite#print_message("[unite-outline] Warning: Please remove the old cache directory: ")
call unite#print_message("[unite-outline] " . s:old_cache_dir)
endif
else " if !isdirectory(s:OUTLINE_CACHE_DIR)
if isdirectory(s:old_cache_dir)
if rename(s:old_cache_dir, s:OUTLINE_CACHE_DIR) != 0
let s:OUTLINE_CACHE_DIR = s:old_cache_dir
call unite#util#print_error("unite-outline: Couldn't rename the cache directory.")
endif
endif
endif
unlet s:old_cache_dir
let s:FILECACHE_FORMAT_VERSION = 2
let s:FILECACHE_FORMAT_VERSION_KEY = '__unite_outline_filecache_format_version__'
let s:BUFVAR_OUTLINE_DATA = 'unite_source_outline_data'
let s:WINVAR_OUTLINE_BUFFER_IDS = 'unite_source_outline_buffer_ids'
"-----------------------------------------------------------------------------
" Functions
function! unite#sources#outline#define() abort
return s:source
endfunction
" Defines an alias of filetype {ftype}.
"
function! unite#sources#outline#alias(alias, ftype) abort
if type(a:alias) == type([])
call s:define_filetype_aliases(a:alias, a:ftype)
elseif type(a:alias) == type('')
call s:define_filetype_aliases([a:alias], a:ftype)
else
call unite#print_error("unite-outline: Unsupported alias type: " . string(a:alias))
endif
endfunction
let s:ftype_alias_table = {}
function! s:define_filetype_aliases(aliases, ftype) abort
for alias in a:aliases
let s:ftype_alias_table[alias] = a:ftype
endfor
endfunction
" Accessor functions for the outline data, that is a Dictionary assigned to
" the buffer local variable.
function! unite#sources#outline#has_outline_data(...) abort
return call('s:has_outline_data', a:000)
endfunction
function! s:has_outline_data(bufnr, ...) abort
if a:0
let key = a:1
let data = getbufvar(a:bufnr, s:BUFVAR_OUTLINE_DATA)
return has_key(data, key)
else
let bufvars = getbufvar(a:bufnr, '')
return has_key(bufvars, s:BUFVAR_OUTLINE_DATA)
endif
endfunction
" Returns the value of outline data {key} for buffer {bufnr}. If the value
" isn't available, returns {default}. When {key} is omitted, returns an
" outline data's Dictionary.
"
function! unite#sources#outline#get_outline_data(...) abort
return call('s:get_outline_data', a:000)
endfunction
function! s:get_outline_data(bufnr, ...) abort
let data = getbufvar(a:bufnr, s:BUFVAR_OUTLINE_DATA)
let argc = len(a:000)
if argc == 0
return data
elseif argc == 1
return data[a:1]
else
return get(data, a:1, a:2)
endif
endfunction
" Sets the value of outline data {key} for buffer {bufnr} to {value}.
"
function! unite#sources#outline#set_outline_data(...) abort
call call('s:set_outline_data', a:000)
endfunction
function! s:set_outline_data(bufnr, key, value) abort
let data = getbufvar(a:bufnr, s:BUFVAR_OUTLINE_DATA)
let data[a:key] = a:value
endfunction
" Removes the value of outline data {key} for buffer {bufnr}.
"
function! unite#sources#outline#remove_outline_data(...) abort
call call('s:remove_outline_data', a:000)
endfunction
function! s:remove_outline_data(bufnr, key) abort
let data = getbufvar(a:bufnr, s:BUFVAR_OUTLINE_DATA)
unlet data[a:key]
endfunction
function! s:has_outline_buffer_ids(winnr) abort
let winvars = getwinvar(a:winnr, '')
return has_key(winvars, s:WINVAR_OUTLINE_BUFFER_IDS)
endfunction
function! s:get_outline_buffer_ids(winnr) abort
let winvars = getwinvar(a:winnr, '')
return winvars[s:WINVAR_OUTLINE_BUFFER_IDS]
endfunction
" Returns the outline info for filetype {ftype}. If not found, returns an
" empty Dictionary. When {ftype} is a Dictionary, assumes it is a Context and
" takes the context buffer's filetype.
"
function! unite#sources#outline#get_outline_info(...) abort
return call('s:get_outline_info', a:000)
endfunction
function! s:get_outline_info(ftype, ...) abort
if type(a:ftype) == type({})
let ftype = join(a:ftype.buffer.filetypes, '.')
let context = a:ftype
else
let ftype = a:ftype
let context = {}
endif
let reload = get(a:000, 0, 0)
let nouser = get(a:000, 1, 0)
let oinfo_paths = (nouser ? s:OUTLINE_INFO_PATH[-1:] : s:OUTLINE_INFO_PATH)
for ftype in s:resolve_filetype(ftype)
if has_key(g:unite_source_outline_info, ftype)
let name = ftype
let oinfo = g:unite_source_outline_info[ftype]
call s:initialize_outline_info(oinfo, ftype)
return oinfo
endif
let load_func = s:find_loadable_func(ftype, oinfo_paths)
if !empty(load_func)
let name = load_func
let oinfo = s:load_outline_info(load_func, context, reload)
if type(oinfo) == type("")
" Redirect.
let redir_ftype = oinfo | unlet oinfo
let oinfo = s:get_outline_info(redir_ftype, reload, nouser)
else
call s:initialize_outline_info(oinfo, load_func)
endif
return oinfo
endif
endfor
return {}
endfunction
function! s:find_loadable_func(ftype, paths) abort
let ftype = substitute(a:ftype, '\.', '_', 'g')
for path in a:paths
let path = substitute(path, '^autoload/', '', '') . ftype
let load_func = substitute(path . '#outline_info', '/', '#', 'g')
try
" NOTE: This is a workaround for that exists('*auto#load#func') always
" returns 0 when used in a function other than the toplevel.
call call(load_func, [])
return load_func
catch /^Vim\%((\a\+)\)\=:E117:/
" E117: Unknown function:
catch
endtry
endfor
return ''
endfunction
" Reloads an autoload script where autoload function {funcname} is defined.
"
function! s:reload_autoload_script(funcname) abort
" path#to#file#func() -> autoload/path/to/file.vim -> full path
let path = 'autoload/' . join(split(a:funcname, '#')[:-2], '/') . '.vim'
let path = get(split(globpath(&runtimepath, path), "\<NL>"), 0, '')
let path = fnamemodify(path, ':p')
" Re-source the autoload script.
execute 'source' fnameescape(path)
endfunction
function! s:load_outline_info(load_func, context, reload) abort
if a:reload
call s:reload_autoload_script(a:load_func)
endif
if !empty(a:context)
try
let oinfo = call(a:load_func, [a:context])
catch /^Vim\%((\a\+)\)\=:E118:/
" E118: Too many arguments for function:
let oinfo = call(a:load_func, [])
endtry
else
let oinfo = call(a:load_func, [])
endif
return oinfo
endfunction
function! s:initialize_outline_info(oinfo, name) abort
if has_key(a:oinfo, 'super')
if a:oinfo.__initialized__ > a:oinfo.super.__initialized__
return
endif
elseif has_key(a:oinfo, '__initialized__')
return
endif
if has_key(a:oinfo, 'initialize')
call a:oinfo.initialize()
endif
call extend(a:oinfo, { 'name': a:name, 'is_volatile': 0 }, 'keep' )
if has_key(a:oinfo, 'skip')
call s:normalize_skip_info(a:oinfo)
endif
call s:normalize_heading_groups(a:oinfo)
if has_key(a:oinfo, 'not_match_patterns')
let a:oinfo.__not_match_pattern__ =
\ '\%(' . join(a:oinfo.not_match_patterns, '\|') . '\)'
endif
if has_key(a:oinfo, 'super')
let a:oinfo.__initialized__ = a:oinfo.super.__initialized__ + 1
else
let a:oinfo.__initialized__ = 1
endif
endfunction
function! s:normalize_skip_info(oinfo) abort
if has_key(a:oinfo.skip, 'header')
let value_type = type(a:oinfo.skip.header)
if value_type == type("")
let a:oinfo.skip.header = { 'leading': a:oinfo.skip.header }
elseif value_type == type([])
let a:oinfo.skip.header =
\ { 'block': s:normalize_block_patterns(a:oinfo.skip.header) }
elseif value_type == type({})
if has_key(a:oinfo.skip.header, 'block') &&
\ type(a:oinfo.skip.header.block) == type([])
let a:oinfo.skip.header.block =
\ s:normalize_block_patterns(a:oinfo.skip.header.block)
endif
endif
endif
if has_key(a:oinfo.skip, 'block')
let value_type = type(a:oinfo.skip.block)
if value_type == type([])
let a:oinfo.skip.block = s:normalize_block_patterns(a:oinfo.skip.block)
endif
endif
endfunction
function! s:normalize_block_patterns(patterns) abort
return { 'begin': a:patterns[0], 'end': a:patterns[1] }
endfunction
function! s:normalize_heading_groups(oinfo) abort
if !has_key(a:oinfo, 'heading_groups')
let a:oinfo.heading_groups = {}
let group_map = {}
else
let groups = keys(a:oinfo.heading_groups)
let group_map = {}
for group in groups
let group_types = a:oinfo.heading_groups[group]
for heading_type in group_types
let group_map[heading_type] = group
endfor
endfor
endif
let group_map.generic = 'generic'
let a:oinfo.heading_group_map = group_map
endfunction
" Returns the value of filetype option {key} for filetype {ftype}.
" If the value isn't available, returns {default}.
"
function! unite#sources#outline#get_filetype_option(...) abort
return call('s:get_filetype_option', a:000)
endfunction
function! s:get_filetype_option(ftype, key, ...) abort
for ftype in s:resolve_filetype(a:ftype)
if has_key(g:unite_source_outline_filetype_options, ftype)
let options = g:unite_source_outline_filetype_options[ftype]
if has_key(options, a:key)
return options[a:key]
endif
endif
endfor
let default = (a:0 ? a:1 : 0)
return get(s:default_filetype_options, a:key, default)
endfunction
" Returns a List of filetypes that are filetype {ftype} itself and its
" fallback filetypes and aliases of each of them.
"
" the given filetype {ftype}
" |/
" (1)aaa.bbb.ccc -> (2)aliases
" (3)aaa/bbb/ccc -> (4)aliases
" |/
" (5)aaa.bbb -> (6)aliases
" (7)aaa/bbb -> (8)aliases
" |/
" (9)aaa -> ($)aliases
"
function! s:resolve_filetype(ftype) abort
let ftcands = []
let ftype = a:ftype
while 1
call add(ftcands, ftype)
let ftcands += s:resolve_filetype_alias(ftype)
if ftype =~ '\.'
let dsl_ftype = substitute(ftype, '\.', '/', 'g')
call add(ftcands, dsl_ftype)
let ftcands += s:resolve_filetype_alias(dsl_ftype)
endif
if ftype =~ '[./]\w\+$'
let ftype = substitute(ftype, '[./]\w\+$', '', '')
else
break
endif
endwhile
call add(ftcands, '*')
return ftcands
endfunction
function! s:resolve_filetype_alias(ftype) abort
let seen = {}
let ftcands = []
let ftype = a:ftype
while 1
if has_key(s:ftype_alias_table, ftype)
if has_key(seen, ftype)
throw "unite-outline: Cyclic alias definition detected."
endif
let ftype = s:ftype_alias_table[ftype]
call add(ftcands, ftype)
let seen[ftype] = 1
else
break
endif
endwhile
return ftcands
endfunction
function! unite#sources#outline#get_highlight(...) abort
return call('s:get_highlight', a:000)
endfunction
function! s:get_highlight(...) abort
for name in a:000
if has_key(g:unite_source_outline_highlight, name)
return g:unite_source_outline_highlight[name]
elseif has_key(s:default_highlight, name)
return s:default_highlight[name]
endif
endfor
return s:default_highlight.normal
endfunction
function! unite#sources#outline#import(name, ...) abort
let name = tolower(substitute(a:name, '\(\l\)\(\u\)', '\1_\2', 'g'))
return call('unite#sources#outline#modules#' . name . '#import', a:000)
endfunction
function! unite#sources#outline#remove_cache_files() abort
call s:FileCache.clear()
endfunction
"-----------------------------------------------------------------------------
" Key-mappings
" DEPRECATED:
nmap <Plug>(unite_source_outline_loop_cursor_down) <Plug>(unite_skip_cursor_down)
nmap <Plug>(unite_source_outline_loop_cursor_up) <Plug>(unite_skip_cursor_up)
"-----------------------------------------------------------------------------
" Variables
if !exists('g:unite_source_outline_info')
let g:unite_source_outline_info = {}
endif
if !exists('g:unite_source_outline_indent_width')
let g:unite_source_outline_indent_width = 2
endif
if !exists('g:unite_source_outline_max_headings')
let g:unite_source_outline_max_headings = 1000
endif
if !exists('g:unite_source_outline_cache_limit')
let g:unite_source_outline_cache_limit = 1000
endif
let s:default_filetype_options = {
\ 'auto_update' : 1,
\ 'auto_update_event': 'write',
\ 'ignore_types' : [],
\ }
if !exists('g:unite_source_outline_filetype_options')
let g:unite_source_outline_filetype_options = {}
endif
let s:default_highlight = {
\ 'comment' : 'Comment',
\ 'expanded': 'Constant',
\ 'function': 'Function',
\ 'id' : 'Special',
\ 'macro' : 'Macro',
\ 'method' : 'Function',
\ 'normal' : 'Normal',
\ 'package' : 'Normal',
\ 'special' : 'Macro',
\ 'type' : 'Type',
\ 'level_1' : 'Type',
\ 'level_2' : 'PreProc',
\ 'level_3' : 'Identifier',
\ 'level_4' : 'Constant',
\ 'level_5' : 'Special',
\ 'level_6' : 'Normal',
\ 'parameter_list': 'Normal',
\ }
if !exists('g:unite_source_outline_highlight')
let g:unite_source_outline_highlight = {}
endif
if !exists('g:unite_source_outline_verbose')
let g:unite_source_outline_verbose = 0
endif
"---------------------------------------
" Aliases
" Define the default filetype aliases.
for [s:ftype, s:aliases] in items(s:OUTLINE_ALIASES)
call call('s:define_filetype_aliases', [s:aliases, s:ftype])
endfor
"-----------------------------------------------------------------------------
" Source
let s:FileCache = unite#sources#outline#import('FileCache', s:OUTLINE_CACHE_DIR)
let s:Tree = unite#sources#outline#import('Tree')
let s:Util = unite#sources#outline#import('Util')
function! s:get_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\d\+_')
endfunction
let s:SID = s:get_SID()
delfunction s:get_SID
let s:outline_buffer_id = 1
let s:source = {
\ 'name' : 'outline',
\ 'description': 'candidates from heading list',
\ 'matchers' : ['outline_matcher_glob', 'outline_formatter'],
\ 'syntax' : 'uniteSource__Outline',
\
\ 'hooks': {}, 'alias_table': {}, 'default_action': {},
\ }
function! s:Source_Hooks_on_init(source_args, unite_context) abort
let a:unite_context.source__outline_buffer_id = s:outline_buffer_id
let a:unite_context.source__outline_source_bufnr = bufnr('%')
call s:initialize_outline_data()
call s:attach_outline_buffer(s:outline_buffer_id)
let s:outline_buffer_id += 1
endfunction
let s:source.hooks.on_init = function(s:SID . 'Source_Hooks_on_init')
" Initialize the current buffer's outline data and register autocommands to
" manage the data if the buffer hasn't been initialized yet.
"
function! s:initialize_outline_data() abort
let bufnr = bufnr('%')
let bufvars = getbufvar(bufnr, '')
if !has_key(bufvars, s:BUFVAR_OUTLINE_DATA)
let bufvars[s:BUFVAR_OUTLINE_DATA] = { 'state': 'OK' }
call s:register_autocmds()
endif
call s:update_buffer_changenr()
endfunction
" Associate the current buffer's window with the outline buffer {buffer_id}
" where the headings from the buffer will be displayed.
"
function! s:attach_outline_buffer(buffer_id) abort
let winnr = winnr()
let winvars = getwinvar(winnr, '')
if !has_key(winvars, s:WINVAR_OUTLINE_BUFFER_IDS)
let winvars[s:WINVAR_OUTLINE_BUFFER_IDS] = []
endif
call add(winvars[s:WINVAR_OUTLINE_BUFFER_IDS], a:buffer_id)
endfunction
function! s:Source_Hooks_on_syntax(source_args, unite_context) abort
let bufnr = a:unite_context.source__outline_source_bufnr
let odata = s:get_outline_data(bufnr)
if type(odata) != type({}) || odata.state !=# 'OK'
return
endif
let oinfo = odata.context.outline_info
if odata.context.extracted_by ==# 'filetype'
" Method: Filetype
if has_key(oinfo, 'highlight_rules')
for hl_rule in oinfo.highlight_rules
if !has_key(hl_rule, 'highlight')
let hl_rule.highlight = s:get_highlight(hl_rule.name)
endif
execute 'syntax match uniteSource__Outline_' . hl_rule.name hl_rule.pattern
\ 'contained containedin=uniteSource__Outline'
execute 'highlight default link uniteSource__Outline_' . hl_rule.name hl_rule.highlight
endfor
endif
else
" Method: Folding
" NOTE: Folding headings are not highlighted at all.
endif
endfunction
let s:source.hooks.on_syntax = function(s:SID . 'Source_Hooks_on_syntax')
function! s:Source_gather_candidates(source_args, unite_context) abort
" Save the Vim options.
let save_cpoptions = &cpoptions
let save_ignorecase = &ignorecase
let save_magic = &magic
try
set cpoptions&vim
set noignorecase
set magic
let bufnr = a:unite_context.source__outline_source_bufnr
let options = s:parse_source_arguments(a:source_args, a:unite_context)
let auto_update = s:get_filetype_option(getbufvar(bufnr, '&filetype'), 'auto_update', 0)
if auto_update
let buffer_changenr = s:get_outline_data(bufnr, 'buffer_changenr', 0)
let model_changenr = s:get_outline_data(bufnr, 'model_changenr', 0)
if model_changenr != buffer_changenr
" The source buffer has been changed since the last extraction.
" Need to update the candidates.
call s:Util.print_debug('event', 'changenr: buffer = ' . buffer_changenr .
\ ', model = ' . model_changenr)
let options.is_force = 1
endif
endif
let candidates = s:get_candidates(bufnr, options)
if auto_update
if get(g:, 'unite_source_outline_event_debug', 0)
let buffer_changenr = s:get_outline_data(bufnr, 'buffer_changenr', 0)
let model_changenr = s:get_outline_data(bufnr, 'model_changenr', 0)
call s:Util.print_debug('event', 'changenr: buffer = ' . buffer_changenr .
\ ', model = ' . model_changenr)
endif
endif
return candidates
catch /^NoWindowError:/
call unite#print_message("[unite-outline] The source buffer has no window.")
return []
catch
if s:get_outline_data(bufnr, 'state') ==# 'OK'
call unite#util#print_error(v:throwpoint)
call unite#util#print_error(v:exception)
call s:set_outline_data(bufnr, 'state', 'Error')
endif
call unite#print_message("[unite-outline] " . v:throwpoint)
call unite#print_message("[unite-outline] " . v:exception)
return []
finally
" Restore the Vim options.
let &cpoptions = save_cpoptions
let &ignorecase = save_ignorecase
let &magic = save_magic
endtry
endfunction
let s:source.gather_candidates = function(s:SID . 'Source_gather_candidates')
function! s:source.complete(args, context, arglead, cmdline, cursorpos) abort "{{{
return s:supported_arguments
endfunction"}}}
function! s:parse_source_arguments(source_args, unite_context) abort
let options = {
\ 'is_force': 0,
\ 'extracted_by': '?',
\ }
for value in a:source_args
if value =~# '^\%(ft\|fi\%[letype]\)$'
let options.extracted_by = 'filetype'
elseif value =~# '^fo\%[lding]$'
let options.extracted_by = 'folding'
elseif value =~# '^\%(update\|!\)$'
let options.is_force = 1
endif
endfor
if a:unite_context.is_redraw
let options.is_force = 1
endif
if has_key(a:unite_context, 'source__outline_swapping')
unlet a:unite_context.source__outline_swapping
let options.is_force = 0
endif
return options
endfunction
" Creates a context Dictionary.
"
function! s:create_context(bufnr, ...) abort
let buffer = {
\ 'nr' : a:bufnr,
\ 'path': fnamemodify(bufname(a:bufnr), ':p'),
\ 'sw' : getbufvar(a:bufnr, '&shiftwidth'),
\ 'ts' : getbufvar(a:bufnr, '&tabstop'),
\ }
let buffer.filetypes = split(getbufvar(a:bufnr, '&filetype'), '\.')
let buffer.filetype = get(buffer.filetypes, 0, '')
let context = {
\ 'trigger': 'user', 'is_force': 0, 'buffer': buffer,
\ 'extracted_by': '?',
\ }
call extend(context, (a:0 ? a:1 : {}))
return context
endfunction
" Returns True if the cached {candidates} is valid and reusable.
"
function! s:is_valid_candidates(candidates, context) abort
let last_method = (!empty(a:candidates) &&
\ a:candidates[0].source__heading_type ==# 'folding' ? 'folding' : 'filetype')
if a:context.extracted_by == '?'
let a:context.extracted_by = last_method
endif
return (a:context.extracted_by ==# last_method)
endfunction
" Returns False if {cache_data}'s format is not compatible with the current
" version of unite-outline.
"
function! s:is_valid_filecache(cache_data) abort
return (type(a:cache_data) == type({})
\ && has_key(a:cache_data, s:FILECACHE_FORMAT_VERSION_KEY)
\ && a:cache_data[s:FILECACHE_FORMAT_VERSION_KEY] == s:FILECACHE_FORMAT_VERSION)
endfunction
function! s:get_candidates(bufnr, options) abort
" Update the context Dictionary.
let context = s:create_context(a:bufnr, a:options)
call s:set_outline_data(a:bufnr, 'context', context)
if context.is_force || !s:has_outline_data(a:bufnr, 'outline_info')
" If triggered by <C-l>, reload the outline info.
let reload = (context.is_force && context.trigger ==# 'user')
let oinfo = s:get_outline_info(context, reload)
call s:set_outline_data(a:bufnr, 'outline_info', oinfo)
else
let oinfo = s:get_outline_data(a:bufnr, 'outline_info')
end
let context.outline_info = oinfo
if !context.is_force && s:has_outline_data(a:bufnr, 'candidates')
" Path A: Get candidates from the buffer local cache.
let candidates = s:get_outline_data(a:bufnr, 'candidates')
if s:is_valid_candidates(candidates, context)
return candidates
endif
endif
if !context.is_force && s:FileCache.has(a:bufnr)
" Path B: Get candidates from the file cache.
try
let cache_data = s:FileCache.get(a:bufnr)
if s:is_valid_filecache(cache_data)
let candidates = cache_data.candidates
if s:is_valid_candidates(candidates, context)
" Save the candidates to the buffer local cache.
call s:set_outline_data(a:bufnr, 'candidates', candidates)
return candidates
endif
endif
" Fallback to Path C.
catch /^unite-outline:/
call unite#util#print_error(v:exception)
endtry
endif
" Path C: Candidates are invalid or haven't been cached, so try to get
" candidates by extracting headings from the buffer.
" Get headings by parsing the buffer.
let headings = s:extract_headings(context)
" Convert the headings into candidates.
let candidates = s:convert_headings_to_candidates(headings, a:bufnr)
let is_volatile = get(oinfo, 'is_volatile', 0)
if !is_volatile
" Save the candidates to the buffer local cache.
call s:set_outline_data(a:bufnr, 'candidates', candidates)
let is_persistant = (context.__num_lines__ > g:unite_source_outline_cache_limit)
if is_persistant
let cache_data = { 'candidates': candidates }
let cache_data[s:FILECACHE_FORMAT_VERSION_KEY] = s:FILECACHE_FORMAT_VERSION
call s:FileCache.set(a:bufnr, cache_data)
elseif s:FileCache.has(a:bufnr)
" Remove the invalid file cache.
call s:FileCache.remove(a:bufnr)
endif
endif
return candidates
endfunction
function! s:extract_headings(context) abort
let src_winnr = bufwinnr(a:context.buffer.nr)
if src_winnr == -1
throw "NoWindowError:"
endif
" Print a progress message.
if a:context.trigger ==# 'auto_update'
if g:unite_source_outline_verbose
call s:Util.print_progress("Update headings...")
endif
else
call s:Util.print_progress("Extract headings...")
endif
" Save the Vim options.
let save_eventignore = &eventignore
let save_winheight = &winheight
let save_winwidth = &winwidth
let save_lazyredraw = &lazyredraw
try
set eventignore=all
" NOTE: To keep the window size on :wincmd, set 'winheight' and 'winwidth'
" to a small value.
let &winheight=&winminheight
let &winwidth=&winminwidth
set lazyredraw
" Switch: current window -> source buffer's window
let cur_winnr = winnr()
execute src_winnr . 'wincmd w'
" Save the cursor and scroll.
let save_cursor = getpos('.')
let save_topline = line('w0')
let lines = [""] + getbufline('%', 1, '$')
let a:context.__num_lines__ = len(lines)
" Merge the temporary context data to the context.
let a:context.lines = lines
let a:context.heading_lnum = 0
let a:context.matched_lnum = 0
let success = 0
let start_time = s:benchmark_start()
" Extract headings.
let s:heading_id = 1
if a:context.extracted_by !=# 'folding'
" Path C_1: Extract headings in filetype-specific way using the
" filetype's outline info.
let a:context.extracted_by = 'filetype'
let headings = s:extract_filetype_headings(a:context)
else
" Path C_2: Extract headings using folds' information.
let a:context.extracted_by = 'folding'
let headings = s:extract_folding_headings(a:context)
endif
" Update the change count of the headings.
call s:set_outline_data(a:context.buffer.nr, 'model_changenr', changenr())
let success = 1
return headings
" Don't catch anything.
finally
" Remove the temporary context data.
unlet! a:context.lines
unlet! a:context.heading_lnum
unlet! a:context.matched_lnum
" Restore the cursor and scroll.
let save_scrolloff = &scrolloff
set scrolloff=0
call cursor(save_topline, 1)
normal! zt
call setpos('.', save_cursor)
let &scrolloff = save_scrolloff
" Switch: current window <- source buffer's window
execute cur_winnr . 'wincmd w'
" Restore the Vim options.
let &lazyredraw = save_lazyredraw
let &winheight = save_winheight
let &winwidth = save_winwidth
let &eventignore = save_eventignore
if success
" Print a progress message.
if a:context.trigger ==# 'auto_update'
if g:unite_source_outline_verbose
call s:Util.print_progress("Update headings...done.")
endif
else
call s:Util.print_progress("Extract headings...done.")
endif
call s:benchmark_stop(start_time, a:context.__num_lines__)
endif
endtry
endfunction
function! s:benchmark_start() abort
if get(g:, 'unite_source_outline_profile', 0) && has("reltime")
return s:get_reltime()
else
return 0
endif
endfunction
function! s:benchmark_stop(start_time, num_lines) abort
if get(g:, 'unite_source_outline_profile', 0) && has("reltime")
let used_time = s:get_reltime() - a:start_time
let used_time_100l = used_time * (str2float("100") / a:num_lines)
call s:Util.print_progress("unite-outline: used=" . string(used_time) .
\ "s, 100l=". string(used_time_100l) . "s")
endif
endfunction
function! s:get_reltime() abort
return str2float(reltimestr(reltime()))
endfunction
" Extract headings from the source buffer in its filetype specific way using
" the filetype's outline info.
"
function! s:extract_filetype_headings(context) abort
let buffer = a:context.buffer
let oinfo = a:context.outline_info
if empty(oinfo)
if empty(buffer.filetype)
call unite#print_message("[unite-outline] Please set the filetype.")
else
call unite#print_message("[unite-outline] " .
\ "Sorry, " . toupper(buffer.filetype) . " is not supported.")
endif
return []
endif
" Extract headings.
if has_key(oinfo, 'before')
call oinfo.before(a:context)
endif
if has_key(oinfo, 'extract_headings')
let headings = oinfo.extract_headings(a:context)
let headings_normalized = 0
else
let headings = s:builtin_extract_headings(a:context)
let headings_normalized = 1
endif
if has_key(oinfo, 'after')
call oinfo.after(a:context)
endif
" Normalize headings.
if type(headings) == type({})
let heading_tree = headings | unlet headings
let headings = s:Tree.flatten(heading_tree)
else
let headings = s:Tree.List.normalize_levels(headings)
endif
if !headings_normalized
call map(headings, 's:normalize_heading(v:val, a:context)')
endif
" Filter headings.
let ignore_types = unite#sources#outline#get_filetype_option(buffer.filetype, 'ignore_types')
let headings = s:filter_headings(headings, ignore_types)
return headings
endfunction
function! s:builtin_extract_headings(context) abort
let oinfo = a:context.outline_info
let [which, pattern] = s:build_heading_pattern(oinfo)
let has_create_heading = has_key(oinfo, 'create_heading')
let num_lines = line('$')
let skip_ranges = s:get_skip_ranges(a:context)
call add(skip_ranges, [num_lines + 1, num_lines + 2]) | " sentinel
let srp = 0 | " skip range pointer
let headings = []
call cursor(1, 1)
while 1
let step = 1
let found = 0
" Search the buffer for the next heading.
let [lnum, col, submatch] = searchpos(pattern, 'cpW')
if lnum == 0
break
endif
while lnum > skip_ranges[srp][1]
let srp += 1
endwhile
if lnum < skip_ranges[srp][0]
if which[submatch] ==# 'heading-1' && lnum < num_lines - 3
" Matched: heading-1
let next_line = getline(lnum + 1)
if next_line =~ '[[:punct:]]\@!\S'
let a:context.heading_lnum = lnum + 1
let a:context.matched_lnum = lnum
let step = 2
let found = 1
elseif next_line =~ '\S' && lnum < num_lines - 4
" See one more next.
let next_line = getline(lnum + 2)
if next_line =~ '[[:punct:]]\@!\S'
let a:context.heading_lnum = lnum + 2
let a:context.matched_lnum = lnum
let step = 3
let found = 1
endif
endif
elseif which[submatch] ==# 'heading'
" Matched: heading
let a:context.heading_lnum = lnum
let a:context.matched_lnum = lnum
let found = 1
elseif which[submatch] ==# 'heading+1' && lnum > 0
" Matched: heading+1
let a:context.heading_lnum = lnum - 1
let a:context.matched_lnum = lnum
let prev_line = getline(lnum - 1)
let found = (prev_line =~ '[[:punct:]]\@!\S')
endif
if found
let heading_line = getline(a:context.heading_lnum)
let matched_line = getline(a:context.matched_lnum)
if has_create_heading
let heading = oinfo.create_heading(
\ which[submatch], heading_line, matched_line, a:context)
else
let heading = {'word':heading_line}
endif
if !empty(heading)
call add(headings, s:normalize_heading(heading, a:context))
endif
if len(headings) >= g:unite_source_outline_max_headings
call unite#print_message("[unite-outline] " .
\ "Too many headings, the extraction was interrupted.")
break
endif
endif
endif
if lnum == num_lines
break
endif
call cursor(lnum + step, 1)
endwhile
return headings
endfunction
" Merge heading-1, heading, heading+1 patterns into one heading pattern for
" use of searchpos().
"
" Example of the return value:
"
" [ ['dummy', 'dummy', 'heading-1', 'heading', 'heading+1'],
" '\%(\(heading-1\)\|\(heading\)\|\(heading+1\)\)' ]
"
function! s:build_heading_pattern(oinfo) abort
let which = ['dummy', 'dummy']
" NOTE: searchpos() returns submatch counted from 2.
let sub_patterns = []
if has_key(a:oinfo, 'heading-1')
call add(which, 'heading-1')
call add(sub_patterns, a:oinfo['heading-1'])
endif
if has_key(a:oinfo, 'heading')
call add(which, 'heading')
call add(sub_patterns, a:oinfo.heading)
endif
if has_key(a:oinfo, 'heading+1')
call add(which, 'heading+1')
call add(sub_patterns, a:oinfo['heading+1'])
endif
call map(sub_patterns, 's:_substitue_sub_pattern(v:val)')
let pattern = '\%(' . join(sub_patterns, '\|') . '\)'
return [which, pattern]
endfunction
function! s:_substitue_sub_pattern(pattern) abort
" Substitute all '\(' with '\%('
let meta_lparen = '\(\(^\|[^\\]\)\(\\\{2}\)*\)\@<=\\('
return '\(' . substitute(a:pattern, meta_lparen, '\\%(', 'g') . '\)'
endfunction
" Returns a List of ranges to be skipped while the extraction.
"
function! s:get_skip_ranges(context) abort
let oinfo = a:context.outline_info
if !has_key(oinfo, 'skip') | return [] | endif
let ranges = []
if has_key(oinfo.skip, 'header')
let header_range = s:get_header_range(a:context)
if !empty(header_range)
call add(ranges, header_range)
endif
endif
if has_key(oinfo.skip, 'block')
let block = oinfo.skip.block
let num_lines = line('$')
call cursor(1, 1)
while 1
let beg_lnum = search(block.begin, 'ceW')
if beg_lnum == 0 || beg_lnum == num_lines
break
endif
let end_lnum = search(block.end, 'W')
if end_lnum == 0
break
endif
call add(ranges, [beg_lnum, end_lnum])
if end_lnum == num_lines
break
else
call cursor(end_lnum + 1, 1)
endif
endwhile
endif
return ranges
endfunction
function! s:get_header_range(context) abort
let oinfo = a:context.outline_info
let header = oinfo.skip.header
let has_leading = has_key(header, 'leading')
let has_block = has_key(header, 'block')
let lnum = 1 | let num_lines = line('$')
while lnum < num_lines
let line = getline(lnum)
if has_leading && line =~# header.leading
let lnum = s:skip_while(header.leading, lnum)
elseif has_block && line =~# header.block.begin
let lnum = s:skip_until(header.block.end, lnum)
else
break
endif
endwhile
let lnum -= 1
if lnum > 1
return [1, lnum]
else
return []
endif
endfunction
function! s:skip_while(pattern, from) abort
let lnum = a:from + 1 | let num_lines = line('$')
while lnum <= num_lines
let line = getline(lnum)
if line !~# a:pattern
break
endif
let lnum += 1
endwhile
return lnum
endfunction
function! s:skip_until(pattern, from) abort
let lnum = a:from | let num_lines = line('$')
while lnum <= num_lines
let line = getline(lnum)
let lnum += 1
if line =~# a:pattern
break
endif
endwhile
return lnum
endfunction
function! s:extract_folding_headings(context) abort
let headings = []
let foldinfo = []
let num_lines = line('$')
" save informaiton of folding.
let lnum = 1
while lnum < num_lines
call add(foldinfo, foldclosed(lnum))
let lnum += 1
endwhile
normal zM
let lnum = 1
while lnum < num_lines
let heading_lnum = foldclosed(lnum)
if heading_lnum != -1
let foldlevel = foldlevel(lnum)
let heading = {
\ 'word' : getline(heading_lnum),
\ 'level': foldlevel,
\ 'type' : 'folding',
\ 'lnum' : heading_lnum,
\ }
call add(headings, s:normalize_heading(heading, a:context))
if len(headings) >= g:unite_source_outline_max_headings
call unite#print_message("[unite-outline] " .
\ "Too many headings, the extraction was interrupted.")
break
endif
" open folding to get information of deeper folding
call cursor(lnum, 0)
foldopen
endif
let lnum += 1
endwhile
" restore folding
let lnum = 1
while lnum < num_lines
if foldinfo[lnum - 1] != -1
call cursor(lnum, 0)
foldclose
let lnum = foldclosedend(lnum) + 1
else
let lnum += 1
endif
endwhile
return headings
endfunction
function! s:normalize_heading(heading, context) abort
let oinfo = a:context.outline_info
let a:heading.id = s:heading_id
let a:heading.word = s:normalize_heading_word(a:heading.word)
call extend(a:heading, {
\ 'level': 1,
\ 'type' : 'generic',
\ 'lnum' : a:context.heading_lnum,
\ 'keyword': a:heading.word,
\ }, 'keep')
let a:heading.line = a:context.lines[a:heading.lnum]
let a:heading.signature = s:calc_signature(a:heading.lnum, a:context.lines)
" group
if !has_key(a:heading, 'group')
let group_map = get(oinfo, 'heading_group_map', {})
let a:heading.group = get(group_map, a:heading.type, 'generic')
endif
" keyword => candidate.word
if has_key(oinfo, '__not_match_pattern__')
let a:heading.keyword =
\ substitute(a:heading.word, oinfo.__not_match_pattern__, '', 'g')
endif
let s:heading_id += 1
return a:heading
endfunction
function! s:normalize_heading_word(word) abort
let word = substitute(substitute(a:word, '^\s*', '', ''), '\s*$', '', '')
let word = substitute(word, '\s\+', ' ', 'g')
return word
endfunction
let s:SIGNATURE_RANGE = 10
let s:SIGNATURE_PRECISION = 2
function! s:calc_signature(lnum, lines) abort
let range = s:SIGNATURE_RANGE
let from = max([1, a:lnum - range])
let to = min([a:lnum + range, len(a:lines) - 1])
let bwd_lines = a:lines[from : a:lnum]
let fwd_lines = a:lines[a:lnum : to]
return s:_calc_signature(bwd_lines, fwd_lines)
endfunction
function! s:_calc_signature(bwd_lines, fwd_lines) abort
let precision = s:SIGNATURE_PRECISION
let is_not_blank = 'v:val =~ "\\S"'
let bwd_lines = filter(a:bwd_lines, is_not_blank)[-precision-1 : -2]
let fwd_lines = filter(a:fwd_lines, is_not_blank)[1 : precision]
return join(map(bwd_lines + fwd_lines, 's:digest_line(v:val)'), '')
endfunction
" Quick and Dirty Digest
function! s:digest_line(line) abort
let line = substitute(a:line, '\s*', '', 'g')
if s:strchars(line) <= 20
let digest = line
else
let line = matchstr(line, '^\%(.\{5}\)\{,20}')
let digest = substitute(line, '\(.\).\{4}', '\1', 'g')
endif
return digest
endfunction
if v:version >= 703
function! s:strchars(str) abort
return strchars(a:str)
endfunction
else
function! s:strchars(str) abort
return strlen(substitute(a:str, '.', 'c', 'g'))
endfunction
endif
" Heading Type Filter
function! s:filter_headings(headings, ignore_types) abort
if empty(a:ignore_types) | return a:headings | endif
let headings = a:headings
let ignore_types = copy(a:ignore_types)
" Remove comment headings.
let idx = index(ignore_types, 'comment')
if idx >= 0
call filter(headings, 'v:val.type !=# "comment"')
let headings = s:Tree.List.normalize_levels(headings)
call remove(ignore_types, idx)
endif
" Remove headings to be ignored.
call map(ignore_types, 'unite#util#escape_pattern(v:val)')
let ignore_types_pattern = '^\%(' . join(ignore_types, '\|') . '\)$'
let pred = 'v:val.type =~# ' . string(ignore_types_pattern)
let headings = s:Tree.List.remove(headings, pred)
return headings
endfunction
function! s:convert_headings_to_candidates(headings, bufnr) abort
if empty(a:headings) | return [] | endif
let path = fnamemodify(bufname(a:bufnr), ':p')
let candidates = map(copy(a:headings), 's:create_candidate(v:val, path)')
return candidates
endfunction
function! s:create_candidate(heading, path) abort
" NOTE:
" abbr - String for displaying
" word - String for narrowing
let indent = repeat(' ', (a:heading.level - 1) * g:unite_source_outline_indent_width)
let cand = {
\ 'abbr': indent . a:heading.word,
\ 'word': a:heading.keyword,
\ 'source': 'outline',
\ 'kind' : 'jump_list',
\ 'action__path': a:path,
\ 'action__line': a:heading.lnum,
\ 'action__pattern': '^' . unite#util#escape_pattern(a:heading.line) . '$',
\ 'action__signature': a:heading.signature,
\ 'source__heading_id': a:heading.id,
\ 'source__heading_level': a:heading.level,
\ 'source__heading_type' : a:heading.type,
\ 'source__heading_group': a:heading.group,
\}
return cand
endfunction
function! s:Source_calc_signature(lnum) abort
let range = s:SIGNATURE_RANGE
let from = max([1, a:lnum - range])
let to = min([a:lnum + range, line('$')])
let bwd_lines = getline(from, a:lnum)
let fwd_lines = getline(a:lnum, to)
return s:_calc_signature(bwd_lines, fwd_lines)
endfunction
let s:source.calc_signature = function(s:SID . 'Source_calc_signature')
"-----------------------------------------------------------------------------
" Auto-update
function! s:register_autocmds() abort
augroup plugin-unite-source-outline
autocmd! * <buffer>
autocmd CursorHold <buffer> call s:on_cursor_hold()
autocmd BufWritePost <buffer> call s:on_buf_write_post()
augroup END
endfunction
augroup plugin-unite-source-outline-win-enter
autocmd!
autocmd BufWinEnter * call s:on_buf_win_enter()
augroup END
function! s:on_cursor_hold() abort
let bufnr = bufnr('%')
if !s:has_outline_data(bufnr)
return
endif
call s:Util.print_debug('event', 'on_cursor_hold at buffer #' . bufnr)
call s:update_buffer_changenr()
if s:should_update('hold')
call s:update_headings(bufnr)
endif
endfunction
function! s:on_buf_write_post() abort
let bufnr = bufnr('%')
if !s:has_outline_data(bufnr)
return
endif
call s:Util.print_debug('event', 'on_buf_write_post at buffer #' . bufnr)
call s:update_buffer_changenr()
if s:should_update('write')
call s:update_headings(bufnr)
endif
endfunction
" Update the change count of the current buffer.
"
function! s:update_buffer_changenr() abort
call s:set_outline_data(bufnr('%'), 'buffer_changenr', changenr())
endfunction
" Returns True if the current buffer has been changed and the headings of the
" buffer should be updated.
"
function! s:should_update(event) abort
let auto_update_enabled = s:get_filetype_option(&l:filetype, 'auto_update')
if !auto_update_enabled
return 0
endif
let auto_update_event = s:get_filetype_option(&l:filetype, 'auto_update_event')
if auto_update_event ==# 'write' && a:event ==# 'hold'
return 0
endif
let bufnr = bufnr('%')
let buffer_changenr = s:get_outline_data(bufnr, 'buffer_changenr', 0)
let model_changenr = s:get_outline_data(bufnr, 'model_changenr', 0)
call s:Util.print_debug('event', 'changenr: buffer = ' . buffer_changenr .
\ ', model = ' . model_changenr)
return (buffer_changenr != model_changenr)
" NOTE: The current changenr may smaller than the last one because undo
" commands decrease the changenr.
endfunction
function! s:update_headings(bufnr) abort
call s:Util.print_debug('event', 'update_headings')
try
" Update the Model data (headings).
call s:get_candidates(a:bufnr, { 'trigger': 'auto_update', 'is_force': 1 })
catch
" Ignore all errors caused by auto-updates because they are so annoying.
" We can know the error details from the error which was caught at
" gather_candidates() not here.
return
endtry
" Update the View (unite.vim' buffer) if the visible outline buffer exists.
let outline_bufnrs = s:find_outline_buffers(a:bufnr)
" NOTE: An outline buffer is an unite.vim's buffer that is displaying the
" candidates from outline source.
for bufnr in outline_bufnrs
call s:Util.print_debug('event', 'redraw outline buffer #' . bufnr)
call unite#force_redraw(bufwinnr(bufnr))
endfor
endfunction
" Returns a List of bufnrs of the outline buffers that are displaying the
" heading list of the buffer {src_bufnr}.
"
function! s:find_outline_buffers(src_bufnr) abort
let outline_bufnrs = []
let bufnr = 1
while bufnr <= bufnr('$')
if bufwinnr(bufnr) > 0
try
" NOTE: This code depends on the current implementation of unite.vim.
if s:is_unite_buffer(bufnr)
let unite = getbufvar(bufnr, 'unite')
let outline_source = s:Unite_find_outline_source(unite)
if !empty(outline_source)
let unite_context = outline_source.unite__context
if unite_context.source__outline_source_bufnr == a:src_bufnr
call add(outline_bufnrs, bufnr)
endif
endif
endif
catch
call unite#util#print_error(v:throwpoint)
call unite#util#print_error(v:exception)
endtry
endif
let bufnr += 1
endwhile
return outline_bufnrs
endfunction
function! s:is_unite_buffer(bufnr) abort
return (bufname(a:bufnr) =~# '^\[unite\]')
endfunction
function! s:Unite_find_outline_source(unite) abort
let result = filter(copy(a:unite.sources), 'v:val.name ==# "outline"')
if empty(result)
return {}
else
return result[0]
endif
endfunction
function! s:on_buf_win_enter() abort
let winnr = winnr()
if !s:has_outline_buffer_ids(winnr)
return
endif
let new_bufnr = bufnr('%')
if s:is_unite_buffer(new_bufnr)
" NOTE: When -no-split.
return
endif
let old_bufnr = bufnr('#')
call s:Util.print_debug('event', 'on_buf_win_enter at window #' . winnr .
\ ' from buffer #' . old_bufnr . ' to #' . new_bufnr)
call s:swap_headings(s:get_outline_buffer_ids(winnr), new_bufnr)
endfunction
" Swaps the heading lists displayed in the outline buffers whose buffer ids
" are one of {outline_buffer_ids} for the heading list of buffer {new_bufnr}.
"
function! s:swap_headings(outline_buffer_ids, new_bufnr) abort
let bufnr = 1
while bufnr <= bufnr('$')
if bufwinnr(bufnr) > 0
try
" NOTE: This code depands on the current implementation of unite.vim.
if s:is_unite_buffer(bufnr)
let unite = getbufvar(bufnr, 'unite')
let outline_source = s:Unite_find_outline_source(unite)
if !empty(outline_source)
let unite_context = outline_source.unite__context
if index(a:outline_buffer_ids, unite_context.source__outline_buffer_id) >= 0
let unite_context.source__outline_source_bufnr = a:new_bufnr
let unite_context.source__outline_swapping = 1
call s:Util.print_debug('event', 'redraw outline buffer #' . bufnr)
call s:initialize_outline_data()
call unite#force_redraw(bufwinnr(bufnr))
endif
endif
endif
catch
call unite#util#print_error(v:throwpoint)
call unite#util#print_error(v:exception)
endtry
endif
let bufnr += 1
endwhile
endfunction
let &cpo = s:save_cpo
unlet s:save_cpo