" Vim global plugin for Perl Analysis, Refactoring, and Tracking
"
" Last change:  Wed May 24 12:26:16 CEST 2017

" Maintainer:   Damian Conway
" License:      This file is placed in the public domain.

" If already loaded, we're done...
if exists("loaded_perlart")
    finish
endif
let loaded_perlart = 1

" Preserve external compatibility options, then enable full vim compatibility...
let s:save_cpo = &cpo
set cpo&vim


"=====[ Refactor a visual block of Perl code ]===============

" INTERFACE...

function! PerlART_SetHighlights ()
    " Set these highlight groups in your .vimrc to change the default appearance

    " Information messages...
    highlight default PerlART_Error                ctermfg=red         cterm=bold
    highlight default PerlART_Problem              ctermfg=black       ctermbg=lightred
    highlight default PerlART_Message              ctermfg=cyan        cterm=bold
    highlight default PerlART_LineNr               ctermfg=blue        cterm=bold

    " How Perl built-in variables will be displayed...
    highlight default PerlART_BuiltIn              ctermfg=lightmagenta

    " How undeclared variables will be displayed...
    highlight default PerlART_Undeclared           ctermfg=red         cterm=bold

    " How declarations of variables that are never used will be displayed...
    highlight default PerlART_Unused               ctermfg=red         cterm=italic
    highlight default PerlART_LexicalDeclUnused    ctermfg=darkred     ctermbg=cyan       cterm=italic
    highlight default PerlART_StaticDeclUnused     ctermfg=darkred     ctermbg=lightblue  cterm=italic
    highlight default PerlART_PackageDeclUnused    ctermfg=darkred     ctermbg=magenta    cterm=italic
    highlight default PerlART_UndeclaredDeclUnused ctermfg=red

    " How the declarations and usages of my variables will be displayed...
    highlight default PerlART_LexicalDecl          ctermfg=black       ctermbg=cyan
    highlight default PerlART_Lexical              ctermfg=cyan

    " How the declarations and usages of state variables will be displayed...
    highlight default PerlART_StaticDecl           ctermfg=black       ctermbg=lightblue
    highlight default PerlART_Static               ctermfg=lightblue

    " How the declarations and usages of our variables will be displayed...
    highlight default PerlART_PackageDecl          ctermfg=black       ctermbg=magenta
    highlight default PerlART_Package              ctermfg=magenta

    " Set this highlight group to anything except Normal, to turn on scope bars...
    highlight default link  PerlART_ScopeBar     Normal

    " Scope bars of different lengths will then be displayed as follows...
    highlight default PerlART_Scope_Small          ctermfg=black       ctermbg=blue
    highlight default PerlART_Scope_Medium         ctermfg=black       ctermbg=darkyellow
    highlight default PerlART_Scope_Large          ctermfg=black       ctermbg=red

    " How multiple selections of code to be refactored will be highlighted...
    highlight default link  PerlART_Selection    Visual

    " How easily confused variable names will be displayed...
    highlight default link  PerlART_Homograms    Normal

    " How easily confused variable names will be displayed...
    highlight default link  PerlART_Parograms    Normal

    " How insufficiently descriptive variable names will be displayed...
    highlight default link  PerlART_Cacograms    Normal

endfunction


" Available keymappings (change these to suit your own preferences)...
function! PerlART_API_setup () abort
    " Rename the variable under the cursor...
    silent nmap     <silent><buffer><expr>      <C-N>        PerlART_RenameVariable()

    " Search for all instances of the variable under the cursor...
    silent nmap     <silent><buffer><expr>      <C-M>        PerlART_MatchAllUses()

    " Jump to the declaration of the variable under the cursor...
    silent nmap     <silent><buffer><expr>      gd           PerlART_GotoDefinition()

    " Jump to the next instance of the variable under the cursor...
    silent nmap     <silent><buffer><special>   *            :silent call PerlART_GotoNextUse()<CR>

    " In visual mode, hoist into a variable all instances of the variable under the cursor...
    silent xnoremap <silent><buffer><expr>      <C-H>        PerlART_HoistExpr('all','variable')

    " In visual mode, hoist into a closure all instances of the variable under the cursor...
    silent xnoremap <silent><buffer><expr>      <C-C>        PerlART_HoistExpr('all','closure')

    " In visual mode, hoist into a subroutine all instances of the variable under the cursor...
    silent xnoremap <silent><buffer><expr>      <C-R>        PerlART_RefactorToSub('all')

    " Doubling the trigger causes only the single instance under the cursor to be refactored...
    silent xnoremap <silent><buffer><expr>      <C-H><C-H>   PerlART_HoistExpr('one','variable')
    silent xnoremap <silent><buffer><expr>      <C-C><C-C>   PerlART_HoistExpr('one','closure')
    silent xnoremap <silent><buffer><expr>      <C-S><C-S>   PerlART_RefactorToSub('one')
endfunction

" These happen automatically...
augroup PerlRefactor
    autocmd!
    autocmd FileType    perl    silent call PerlART_API_setup()
    autocmd FileType    perl    silent call PerlART_SetHighlights()
    autocmd ColorScheme *       if &filetype == 'perl' | silent call PerlART_SetHighlights() | endif

    autocmd CursorHold  *.p[lm],*.t  call PerlART_RunVarAnalysis()
    autocmd CursorHold  *.p[lm],*.t
    \    if get(b:,'PerlART_tick',-1) < b:changedtick | call PerlART_RunCodeAnalysis() | endif
augroup END


" Set this variable in your .vimrc to preconfigure the Perl-based subroutine refactoring...
" For example, to change the default names for refactored subroutines and hoisted lexicals:
"
"    let g:PerlART_sub_name   = "NEW_SUB"


"=======================================================================
" IMPLEMENTATION...


"=====[ Variable renaming ]=========

function! PerlART_VarRename () abort
    call setpos("'r", getcurpos())

    " Find the character offset of the desired (cursored) variable in the source code...
    let var_offset = wordcount()['cursor_chars'] - 1

    " Grab the entire source code from the buffer...
    let src = join(getline(1,'$'), "\n")

    " Get the full name of the variable under the cursor...
    let cmd = printf("perl -MCode::ART -E'get_variable_for_Vim(%d)'", var_offset)
    let var_name = substitute(system(cmd, src), '\n', '', 'g')
    if var_name == ""
        echohl WarningMsg
        echo "Can't rename there (cursor is not over a variable)"
        echohl NONE
        return
    endif

    " Ask for the new name...
    call inputsave()
    echohl WarningMsg
    let new_name = input('Rename ' . var_name . ' to: ' . var_name[0])
    echohl NONE
    call inputrestore()

    " Allow them to cancel by entering a blank name...
    if new_name =~ "^\s*$"
        return
    endif

    " Call out to Code::ART to do the hard work...
    let cmd = printf(
    \   "perl -MCode::ART -E'rename_variable_for_Vim(%d, q{%s})'", var_offset, new_name
    \)
    let new_lines = systemlist(cmd, src)

    " Report any failure or install the updated code...
    if new_lines[0] =~ '^----'
        echohl WarningMsg
        echo strpart(new_lines[0],4)
        echohl NONE
    else
        call setline(line('.'), '')
        call setline(1, new_lines)
    endif
endfunction

"=====[ Code refactoring ]=========

let s:MISSING_RETURN_STATEMENT = '# RETURN VALUE HERE?'

" Provide list of possible variables to complete return statement...
function! PerlART_complete (ArgLead, CmdLine, CursorPos)
    return b:PRcomplete_vars
endfunction

" Do the refactoring...
function! PerlART_RefactorToSub (what) range
    return "\"sygv:\<C-U>'<,'>call PerlART_perform_refactor('".a:what."', '".mode()."')\<CR>"
endfunction

function! PerlART_perform_refactor (what, mode) abort range
    " Get the old code's location...
    let [buf, startline, startcol, etc] = getpos("'<")
    let [buf,   endline,   endcol, etc] = getpos("'>")
    if a:mode ==# 'V'
        let [startbyte, endbyte] = [line2byte(startline)-1, line2byte(endline+1)-2]
    else
        let [startbyte, endbyte] = [line2byte(startline)+startcol-2, line2byte(endline)+endcol-2]
    endif

    " Save original target code as a searchable pattern and highlight all instances...
    if a:what == 'all'
        let target_code = '\M'.escape(trim(@s), "\\")
        call PerlART_matchadd('PerlART_Selection', target_code, 100)
        redraw
    endif

    " Get the new sub's name...
    let how_much = a:what == 'all' ? ' every instance of this code ' : ' this code only '
    let newname = Ask("Refactor".how_much."as: sub ", get(g:, 'PerlART_sub_name', 'SUBNAME'))
    if newname == "\<ESC>"
        call PerlART_matchclear('PerlART_Selection')
        redraw!
        normal! gv
        return
    endif


    " Set up the arguments for the Perl script that does all the hard work...
    let options = '{ name => q{' . newname   . '}, '
    \           . '  from => '   . startbyte . ',  '
    \           . '  to   => '   . endbyte   . ',  '
    \           . '}'

    " Call the script and unpack the results...
    let refactored = eval(
                   \    substitute(
                   \      system(
                   \         "perl -MCode::ART::API::Vim -e 'refactor_to_sub(" . options . ")'",
                   \         join(getline(1, '$'), "\n")
                   \      ),
                   \      '\n', '', 'g'
                   \    )
                   \ )

    if has_key(refactored, 'failed')
        echohl  PerlART_Error
        echomsg "Can't refactor selected code (" . refactored['failed'] . ")"
        echohl  NONE
        call PerlART_matchclear('PerlART_Selection')
        normal gv
        return
    endif

    let refactored_call   = refactored['call']
    let refactored_code   = refactored['code']
    let return_candidates = refactored['return']

    " Prompt for a return statement, if one seems to be needed...
    if refactored_code =~ s:MISSING_RETURN_STATEMENT
        let b:PRcomplete_vars = join(keys(return_candidates), "\n")
        call inputsave()
        let return_val = input("Return statement: return ", "", "custom,PerlART_complete")
        call inputrestore()
        if !empty(return_val)
            let refactored_code
            \   = substitute(refactored_code,
            \                s:MISSING_RETURN_STATEMENT,
            \                'return ' . get(return_candidates, return_val, escape(return_val,'\')) . ';', '')
        else
            let refactored_code
            \   = substitute(refactored_code, '\_s*'.s:MISSING_RETURN_STATEMENT.'\_s*', "\n", '')
        endif
    endif

    " Install the replacement code...
    let @s = refactored_call
    if a:mode ==? 'v'
        silent normal! gv"sp
    else
        silent normal! gvv"sp
    endif

    " Install everywhere, if requested...
    call PerlART_matchclear('PerlART_Selection')
    if a:what == 'all'
        try
            silent exec 'silent %s/' . escape(target_code, '/') . '/' . escape(trim(@s),'\\/') . '/g'
        catch
        endtry
    endif

    " Put the subroutine definition into the nameless and "s registers, ready for pasting
    let @" = refactored_code . "\n"
    let @s = refactored_code . "\n"

endfunction

let s:PerlART_highlight = {
\    'my' : 'PerlART_Lexical',
\ 'state' : 'PerlART_Static',
\   'our' : 'PerlART_Package',
\   'sub' : 'PerlART_Lexical',
\   'for' : 'PerlART_Package'
\}

function! PerlART_RunVarAnalysis () abort
    " Kill any incomplete analysis...
    if has_key(b:,'PerlART_RVA_job')
        call job_stop(b:PerlART_RVA_job)
    endif

    " Start a new analysis...
    let code = 'classify_var_at('.wordcount()['cursor_chars'].');'
    let b:PerlART_RVA_job
        \ = job_start(['perl', '-MCode::ART::API::Vim', '-E', code],
                      \{"in_io" : "buffer", "in_name" : "%", "out_cb": "PerlART_HandleVarAnalysis"})
endfunction

let s:PerlART_MatchID_Decl      = 664668
let s:PerlART_MatchID_Usage     = 668664
let s:PerlART_MatchID_Homograms = 665667
let s:PerlART_MatchID_Parograms = 663668
let s:PerlART_MatchID_ScopeBar  = 667665

function! PerlART_HandleVarAnalysis (channel, msg)
    let b:PerlART_cursvar = eval(a:msg)

    for m in getmatches()
        if m['id'] =~ '66\d66\d'
            call matchdelete(m['id'])
        endif
    endfor

    if has_key(b:PerlART_cursvar, 'failed')
        echo
        redraw
        return
    endif

    let declarator = get(b:PerlART_cursvar, 'declarator', '')
    let declloc    = get(b:PerlART_cursvar, 'declared_at', -1)
    if empty(declarator) && declloc >= 0
        let declarator = 'my'
    endif
    let is_undeclared = b:PerlART_cursvar['declared_at'] < 0
    \                && !b:PerlART_cursvar['is_builtin']
    \                && b:PerlART_cursvar['raw_name'] !~ '::\|'''

    let hl = get(s:PerlART_highlight, declarator, ( b:PerlART_cursvar['is_builtin']      ? 'PerlART_BuiltIn'
             \                               : b:PerlART_cursvar['raw_name'] =~ '::\|''' ? 'PerlART_Package'
             \                               :                                     'PerlART_Undeclared'
             \                               ))
    if declloc >= 0
        if empty(b:PerlART_cursvar['used_at'])
            call PerlART_matchadd(hl.'DeclUnused', b:PerlART_cursvar['declloc'].b:PerlART_cursvar['matchname'], 10, s:PerlART_MatchID_Decl)
        else
            call PerlART_matchadd(hl.'Decl', b:PerlART_cursvar['declloc'].b:PerlART_cursvar['matchname'], 10, s:PerlART_MatchID_Decl)
        endif
    endif
    call PerlART_matchadd(hl, b:PerlART_cursvar['matchloc'].b:PerlART_cursvar['matchname'], 9, s:PerlART_MatchID_Usage)

    if b:PerlART_cursvar['homograms'] != ''
        let homograms = '\%(' . b:PerlART_cursvar['homograms'] . '\)'
        call PerlART_matchadd('PerlART_Homograms', homograms.'\k\@!''\@!', 0, s:PerlART_MatchID_Homograms)
    endif

    if b:PerlART_cursvar['parograms'] != ''
        let parograms = '\%(' . b:PerlART_cursvar['parograms'] . '\)'
        call PerlART_matchadd('PerlART_Parograms', parograms.'\k\@!''\@!', 0, s:PerlART_MatchID_Parograms)
    endif

    if synIDtrans(hlID('PerlART_ScopeBar')) != synIDtrans(hlID('Normal'))
        if b:PerlART_cursvar['scope_size']  < 10
            call PerlART_matchadd('PerlART_Scope_Small',  '\%1c'.b:PerlART_cursvar['scopeloc'], 102, s:PerlART_MatchID_ScopeBar)
        elseif b:PerlART_cursvar['scope_scale'] < 0.2
            call PerlART_matchadd('PerlART_Scope_Medium', '\%1c'.b:PerlART_cursvar['scopeloc'], 102, s:PerlART_MatchID_ScopeBar)
        else
            call PerlART_matchadd('PerlART_Scope_Large',  '\%1c'.b:PerlART_cursvar['scopeloc'], 102, s:PerlART_MatchID_ScopeBar)
        endif
    endif

    let linenum_width = strlen(line('$'))
    let linenum = get(b:PerlART_cursvar, 'declared_at', -1) >= 0 ? byte2line(b:PerlART_cursvar['declared_at']-1)
    \                                                    : repeat('-', linenum_width)
    echohl PerlART_LineNr
    echo printf('%*s: ', linenum_width, linenum)

    exec 'echohl ' . hl
    echon (empty(declarator) ? '' : declarator . ' ')
       \. (declarator ==# 'sub' ? '(' . get(b:PerlART_cursvar, 'decl_name', '') . ')' : get(b:PerlART_cursvar, 'decl_name', '') )
       \. ( !empty(get(b:PerlART_cursvar, 'desc', ''))  ? '   # ' . b:PerlART_cursvar['desc'] : '' )
       \. ( !len(b:PerlART_cursvar['used_at'])          ? '  [unused]'
       \  : is_undeclared                       ? '  [undeclared]'
       \  : b:PerlART_cursvar['is_cacogram']
       \    && synIDtrans(hlID('PerlART_Cacograms')) != synIDtrans(hlID('Normal'))
       \                                        ? '  [needs a more descriptive name?]'
       \  :                                       ''
       \  )
    echohl NONE

    if &foldexpr == 'FS_FoldSearchLevel()'
        let @/ = b:PerlART_cursvar['matchloc'].b:PerlART_cursvar['matchname']
        normal zx
    endif
endfunc

function! PerlART_RenameVariable () abort
    " Are we actually on a variable?
    if has_key(b:PerlART_cursvar, 'failed')
        echohl PerlART_Error
        echo 'Please place the cursor over a variable and try again'
        echohl NONE
        return ''
    endif

    " What's the new name???
    func! PerlART_rename_aliases (A,C,P)
        return map(keys(get(b:PerlART_cursvar,'aliases',{})), {_,v -> strpart(v,1)})
    endfunc
    echohl PerlART_Message
    let g:new_name = input('Rename '.b:PerlART_cursvar['decl_name'].' --> '.b:PerlART_cursvar['sigil'],
                \          '', 'customlist,PerlART_rename_aliases')
    echohl NONE

    " A blank input cancels the rename...
    if g:new_name =~ '^\s*$'
        echohl PerlART_Warning
        echo 'Rename cancelled'
        echohl NONE
        return ''
    endif

    if b:PerlART_cursvar['is_builtin'] && !has_key(b:PerlART_cursvar['aliases'], b:PerlART_cursvar['sigil'].g:new_name)
        if Ask( 'Globally renaming ' . b:PerlART_cursvar['decl_name']
        \     . ' to '.  b:PerlART_cursvar['sigil'].g:new_name
        \     . " will remove its special behaviour. Proceed anyway? [yn] ", 'no'
        \     ) !~ '^\s*[Yy]'
            echohl PerlART_Error
            echo 'Rename cancelled'
            echohl NONE
            return ''
        endif
    endif

    return ':%s/' . b:PerlART_cursvar['matchloc'].b:PerlART_cursvar['matchnameonly'] . '/\=g:new_name/g' . "\<CR>``"
endfunction

function! PerlART_GotoDefinition () abort
    if has_key(b:PerlART_cursvar, 'failed')
        echohl PerlART_Error
        echo   'Please place the cursor over a variable and try again'
        echohl NONE
        return ""
    elseif get(b:PerlART_cursvar, 'declared_at', -1) < 0
        echohl PerlART_Error
        echo   'This variable has no declaration in the current file'
        echohl NONE
        return ""
    else
        return '/'.b:PerlART_cursvar['declloc'].b:PerlART_cursvar['matchname']."\<CR>"
    endif
endfunction

function! PerlART_GotoNextUse () abort
    if has_key(get(b:,'PerlART_cursvar',{'failed':1}), 'failed')
        silent normal! *
    else
        let @/ = b:PerlART_cursvar['matchloc'].b:PerlART_cursvar['matchname']
        normal n
    endif
endfunction

function! PerlART_MatchAllUses () abort
    if !has_key(b:PerlART_cursvar, 'failed')
        let @/ = b:PerlART_cursvar['matchloc'].b:PerlART_cursvar['matchname']
        return "/\<CR>``"
    endif
endfunction

function! PerlART_HoistExpr (one_all, kind) range
    return '"vygv'
         \ . ":\<C-U>'<,'>call PerlART_Impl_HoistExpr('".mode()."',".(a:one_all=='all').",'".a:kind."')\<CR>"
endfunction

function! PerlART_Impl_HoistExpr (mode, all, kind) abort range
    " We may need to change the kind later...
    let kind = a:kind

    " Get the old code's location...
    let [buf, startline, startcol, etc] = getpos("'<")
    let [buf,   endline,   endcol, etc] = getpos("'>")
    if a:mode ==# 'V'
        let [startbyte, endbyte] = [line2byte(startline), line2byte(endline+1)-1]
    else
        let [startbyte, endbyte] = [line2byte(startline)+startcol-1, line2byte(endline)+endcol-1]
    endif


    " Analyze the file to locate replaceable instances of the expression...
    let expr_scope = eval(
    \   system('perl -MCode::ART::API::Vim -e"find_expr_scope('.startbyte.','.endbyte.','.a:all.')"',
    \          join(getline(1,'$'),"\n"))
    \)

    " Can't hoist the selection (not an expression)...
    if has_key(expr_scope, 'failed')
        echohl PerlART_Error
        echomsg "Can't hoist "
        \  . (a:all ? 'multiple instances of that expression' : 'that expression')
        \  . " (because " . expr_scope['failed'] . ')'
        echohl None
        return
    endif

    " Handle mutators...
    if kind == 'variable' && expr_scope['mutators'] > 0 && expr_scope['matchcount'] > 1
        let kind = 'closure'
    endif

    " Show the targets for hoisting...
    let multiselect = matchadd('PerlART_Selection', expr_scope['matchloc'], 100)
    redraw

    " Get default name
    let default_name = substitute(@v, '^\W\+\|\W\+$', '', 'g')
    let default_name = substitute(default_name, '\W\+', '_', 'g')
    if default_name !~ '\a'
        let default_name = 'variable'
    endif
    let default_name = (kind == 'variable' ? '$' : '') . default_name
    if strchars(default_name) > 30
        let default_name = strcharpart(default_name,0,20) . '_etc'
    endif

    " Detemine the name of the new hoist variable...
    let varname = Ask( 'Hoist '
                \    . (a:all && expr_scope['matchcount'] > 1
                \          ? 'all these expressions'
                \          : 'this expression only' )
                \    . ' to a ' . kind . ' named: ',
                \      default_name)
    let varname  = substitute(varname, '^\s\+', '', '')
    let varsubst = varname
    if varname == ""
        let varname  = default_name
        let varsubst = default_name
    endif
    if varname !~ '^[$@%]'
        if kind == 'variable'
            let varname  = '$'.varname
            let varsubst = varname
        elseif kind == 'closure' && get(expr_scope,'use_version',0) < 5.026
            let varname  = '$'.varname
            let varsubst = varname . '->()'
        elseif kind == 'closure'
            let varsubst = varname . '()'
        endif
    endif

    " Stop showing the targets (they're about to disappear anyway"
    call matchdelete(multiselect)

    " Prevent inserted lines from wrapping (badly)...
    let textwidth = &textwidth
    let &textwidth = 1000000

    " Replace each target with the hoist variable...
    exec "silent :%s/" . expr_scope['matchloc'] . '/' . varsubst .  "/"

    " Go to the most logical place and insert the hoist variable's definition...
    exec "?" . expr_scope['firstloc']
    if kind == 'variable'
        exec "silent normal Omy " . varname . " = " . expr_scope['target'] . ";\<ESC>V"
    elseif kind == 'closure'
        if get(expr_scope,'use_version',0) < 5.026
            exec "silent normal Omy " . varname . " = sub { " . expr_scope['target'] . " };\<ESC>V"
        else
            exec "silent normal Omy sub " . varname . " { " . expr_scope['target'] . " }\<ESC>V"
        endif
    endif

    " Leave the campsite exactly as we found it...
    let &textwidth = textwidth
endfunction

function! PerlART_matchadd (group, pattern, priority, ...) abort
    call PerlART_matchclear(a:group)
    let b:PerlART_matchID[a:group] =
    \   a:0 > 1 ? matchadd(a:group, a:pattern, a:priority, a:1, a:2)
    \ : a:0 > 0 ? matchadd(a:group, a:pattern, a:priority, a:1)
    \ :           matchadd(a:group, a:pattern, a:priority)
endfunction

function! PerlART_matchclear (group) abort
    if !has_key(b:,'PerlART_matchID')
        let b:PerlART_matchID = {}
    endif
    if has_key(b:PerlART_matchID, a:group)
        try | call matchdelete(b:PerlART_matchID[a:group]) | catch /./ | endtry
    endif
endfunction

function! PerlART_RunCodeAnalysis () abort
    " Kill any incomplete analysis...
    if has_key(b:,'PerlART_RCA_job')
        call job_stop(b:PerlART_RCA_job)
    endif

    " Start a new analysis...
    let b:PerlART_RCA_job
        \ = job_start(['perl', '-MCode::ART::API::Vim', '-E', 'analyze_code()'],
                      \{"in_io" : "buffer", "in_name" : "%", "out_cb": "PerlART_HandleCodeAnalysis"})
endfunction

function! PerlART_HandleCodeAnalysis (channel, msg)
    let b:PerlART_tick = b:changedtick
    let b:PerlART_analysis = eval(a:msg)
    if has_key(b:PerlART_analysis, 'failed')
        return
    endif

    if b:PerlART_analysis['cacograms'] != ''
        call PerlART_matchadd('PerlART_Cacograms',      b:PerlART_analysis['cacograms'],     -2)
    else
        call PerlART_matchclear('PerlART_Cacograms')
    endif
    if b:PerlART_analysis['undeclared_vars'] != ''
        call PerlART_matchadd('PerlART_Undeclared', b:PerlART_analysis['undeclared_vars'], -1)
    else
        call PerlART_matchclear('PerlART_Undeclared')
    endif
    if b:PerlART_analysis['unused_vars'] != ''
        call PerlART_matchadd('PerlART_Unused',     b:PerlART_analysis['unused_vars'],     -1)
    else
        call PerlART_matchclear('PerlART_Unused')
    endif
endfunc


"=====[ Utility function for cmdline interaction ]===========

" Default highlight groups
highlight default AskPrompt  ctermfg=white cterm=bold
highlight default AskDefault ctermfg=blue  cterm=bold,italic
highlight default AskInput   ctermfg=cyan

" Get a character, ignoring annoying timeouts...
function! s:active_getchar () abort

    " Is there anything to get...
    let char = getchar()

    " Skip any CursorHold timeouts, by rechecking...
    while char == "\<CursorHold>"
      let char = getchar()
    endwhile

    " Translate <DELETE>'s...
    if char == 128 || char == "\<BS>"
        return "\<BS>"
    endif

    " See if we got a single character, otherwise return the lot...
    let single_char = nr2char(char)
    return empty(single_char) ? char : single_char
endfunction

" Like the built-in input() function, only prettier and smarter...
function! Ask (prompt, ...) abort
    " Remember where we parked...
    call inputsave()

    " Clean up the prompt...
    let preprompt = split(substitute(a:prompt, '\s*$', ' ', ''), "\n", 1)
    let prompt = remove(preprompt, -1)
    let default = get(a:000,0,'')

    " Echo it, with any default in a different colour
    echohl AskPrompt
    for line in preprompt
        echo line
    endfor

    echohl AskDefault
    echo prompt . default
    echohl AskPrompt
    echon "\r" . prompt
    echohl NONE
    let first = 1
    let input = ''
    while 1
        let next_char = s:active_getchar()
        if first
            echohl AskPrompt
            echon "\r" . prompt . repeat(' ', strchars(default))
            echon "\r" . prompt
            echohl NONE
            let first = 0
        endif
        if next_char == "\<ESC>" || next_char == "\<C-C>"
            call inputrestore()
            return next_char
        elseif next_char == "\<BS>"
            let input = strpart(input,0,strchars(input)-1)
            echohl AskPrompt
            echon "\r" . prompt
            echohl AskInput
            echon input . ' '
            echohl NONE
        elseif next_char == "\<CR>"
            call inputrestore()
            return (strchars(input) ? input : default)
        else
            let input .= next_char
        endif

        " Redraw default if no input...
        if strchars(input) == 0
            echohl AskDefault
            echon "\r" . prompt . default
        endif

        " Redraw prompt and any input...
        echohl AskPrompt
        echon "\r" . prompt
        if strchars(input) > 0
            echohl AskInput
            echon input
        endif
        echohl NONE
    endwhile
endfunction



" Restore previous external compatibility options
let &cpo = s:save_cpo