diff --git a/autoload/fish.vim b/autoload/fish.vim index 2c4d894..63c4681 100644 --- a/autoload/fish.vim +++ b/autoload/fish.vim @@ -1,23 +1,122 @@ +function! s:IsString(lnum, col) + " Returns "true" if syntax item at the given position is part of fishString. + let l:stack = map(synstack(a:lnum, a:col), 'synIDattr(v:val, "name")') + return len(filter(l:stack, 'v:val ==# "fishString"')) +endfunction + +function! s:IsContinuedLine(lnum) + " Returns "true" if the given line is a continued line. + return getline(a:lnum - 1) =~ '\v\\$' +endfunction + +function! s:IsInSubstitution(lnum) + " Returns "true" if the given line is a continued line. + return getline(a:lnum - 1) =~ '\v\($' +endfunction + +function! s:FindPrevLnum(lnum) + " Starting on the given line, search backwards for a line that is not + " empty, not part of a string and not a continued line. + if a:lnum < 1 || a:lnum > line('$') + " First line or wrong value, follow prevnonblank() behaviour and + " return zero. + return 0 + endif + let l:lnum = prevnonblank(a:lnum) + while l:lnum > 0 && ( s:IsContinuedLine(l:lnum) || s:IsString(l:lnum, 1) ) + let l:lnum = prevnonblank(l:lnum - 1) + endwhile + return l:lnum +endfunction + +function! s:IsSwitch(lnum) + " Returns "true" if the given line is part of a switch block. + let l:lnum = a:lnum + let l:line = getline(l:lnum) + let l:in_block = 0 + let l:stop_pat = '\v^\s*%(if|else|while|for|begin)>' + let l:block_start_pat = '\v^\s*%(if|while|for|switch|begin)>' + while l:lnum > 0 + let l:lnum = prevnonblank(l:lnum - 1) + let l:line = getline(l:lnum) + if l:line =~# '\v^\s*end>' + let l:in_block += 1 + elseif l:in_block && l:line =~# l:block_start_pat + let l:in_block -= 1 + elseif !l:in_block && l:line =~# l:stop_pat + return 0 + elseif !l:in_block && l:line =~# '\v^\s*switch>' + return 1 + endif + endwhile + return 0 +endfunction + function! fish#Indent() - let l:shiftwidth = shiftwidth() - let l:prevlnum = prevnonblank(v:lnum - 1) - if l:prevlnum ==# 0 + let l:line = getline(v:lnum) + if s:IsString(v:lnum, 1) + return indent(v:lnum) + endif + " shiftwidth can be misleading in recent versions, use shiftwidth() if + " it is available. + if exists('*shiftwidth') + let l:shiftwidth = shiftwidth() + else + let l:shiftwidth = &shiftwidth + endif + let l:prevlnum = s:FindPrevLnum(v:lnum - 1) + if l:prevlnum == 0 return 0 endif - let l:indent = 0 + let l:shift = 0 let l:prevline = getline(l:prevlnum) - if l:prevline =~# '\v^\s*switch>' - let l:indent = l:shiftwidth * 2 - elseif l:prevline =~# '\v^\s*%(begin|if|else|while|for|function|case)>' - let l:indent = l:shiftwidth + let l:previndent = indent(l:prevlnum) + if s:IsContinuedLine(v:lnum) + " It is customary to increment indentation of continued lines by four + " or a custom value defined by the user if available. + let l:previndent = indent(v:lnum - 1) + if s:IsContinuedLine(v:lnum - 1) + return l:previndent + elseif exists('g:fish_indent_cont') + return l:previndent + g:fish_indent_cont + elseif exists('g:indent_cont') + return l:previndent + g:indent_cont + else + return l:previndent + l:shiftwidth + endif endif - let l:line = getline(v:lnum) - if l:line =~# '\v^\s*end>' - return indent(v:lnum) - (l:indent ==# 0 ? l:shiftwidth : l:indent) - elseif l:line =~# '\v^\s*%(case|else)>' - return indent(v:lnum) - l:shiftwidth + if l:prevline =~ '(\s*$' + " Inside a substitution + let l:shift += 1 + endif + if l:line =~ '^\s*)' + " Outside a substitution + let l:shift -= 1 + endif + if l:prevline =~# '\v^\s*%(begin|if|else|while|for|function|case|switch)>' + " First line inside a block, increase by one. + let l:shift += 1 + endif + if l:line =~# '\v^\s*%(end|case|else)>' + " "end", "case" or "else", decrease by one. + let l:shift -= 1 + endif + if l:line =~# '\v^\s*' && l:prevline =~# '\v' + " "case" following "switch", increase by one. + let l:shift += 1 + endif + if l:line =~# '\v\s*end>' && l:prevline !~# '\v' && s:IsSwitch(v:lnum) + " "end" ends switch block, but not immediately following "switch" + " decrease by one more so it matches the indentation of "switch". + let l:shift -= 1 + endif + if l:prevline =~# '\v^\s*%(if|while|for|else|switch|end)>.*' + " "begin" after start of block, increase by one. + let l:shift += 1 endif - return indent(l:prevlnum) + l:indent + let l:indent = l:previndent + l:shift * l:shiftwidth + " Only return zero or positive numbers. + return l:indent < 0 ? 0 : l:indent endfunction function! fish#Format() @@ -27,6 +126,8 @@ function! fish#Format() let l:command = v:lnum.','.(v:lnum+v:count-1).'!fish_indent' echo l:command execute l:command + " Fix indentation and replace tabs with spaces if necessary. + normal! '[='] endif endfunction @@ -50,18 +151,63 @@ function! fish#Complete(findstart, base) endif let l:results = [] let l:completions = - \ system('fish -c "complete -C'.shellescape(a:base).'"') - let l:cmd = substitute(a:base, '\v\S+$', '', '') - for l:line in split(l:completions, '\n') + \ system('fish -c "complete -C'.shellescape(a:base).'"') + let l:sufpos = match(a:base, '\v\S+$') + if l:sufpos >= 0 + let l:cmd = a:base[:l:sufpos-1] + let l:arg = a:base[l:sufpos:] + else + let l:cmd = a:base + let l:arg = '' + endif + for l:line in filter(split(l:completions, '\n'), '!empty(v:val)') let l:tokens = split(l:line, '\t') - call add(l:results, {'word': l:cmd.l:tokens[0], - \'abbr': l:tokens[0], - \'menu': get(l:tokens, 1, '')}) + let l:term = l:tokens[0] + if l:term =~? '^\V'.l:arg + call add(l:results, { + \ 'word': l:cmd.l:term, + \ 'abbr': l:term, + \ 'menu': get(l:tokens, 1, ''), + \ 'dup': 1 + \ }) + endif endfor return l:results endif endfunction function! fish#errorformat() - return '%Afish: %m,%-G%*\\ ^,%-Z%f (line %l):%s' + return '%A<%t> fish: %m,%Efish: %m,%E%f (line %l): %m,%E%f (line %l):%.%#,%-Z%p^,%Ein %m,%Z called on line %l of file %f,%Ein %m,%C%s,%-G%.%#' +endfunction + +function! fish#Help(ref) abort + let l:ref = a:ref + if empty(a:ref) + " let l:ref = &filetype ==# 'man' ? expand('') : expand('') + let l:ref = expand('') + if empty(l:ref) + call s:fish_help_error('no identifier under cursor') + return + endif + endif + let l:output = systemlist('fish -c "man -w ' . shellescape(l:ref) . '"') + if v:shell_error + call s:fish_help_error(printf('command exited with code %d: %s', v:shell_error, join(l:output))) + return + endif + aug ft_man_fish + au FileType man + \ setlocal nobuflisted + \ | setlocal keywordprg=:FishHelp' + \ | nnoremap K :FishHelp + \ | nnoremap :FishHelp + aug END + execute 'Man ' . l:output[0] + silent aug! ft_man_fish +endfunction + +function! s:fish_help_error(message) + echohl ErrorMsg + echon 'FishHelp: ' a:message + echohl NONE endfunction diff --git a/bin/man.fish b/bin/man.fish index f8cbdfd..eede386 100755 --- a/bin/man.fish +++ b/bin/man.fish @@ -1 +1 @@ -man $argv +man $argv | col -bf diff --git a/compiler/fish.vim b/compiler/fish.vim index 21fad71..c2b370d 100644 --- a/compiler/fish.vim +++ b/compiler/fish.vim @@ -4,4 +4,4 @@ endif let current_compiler = 'fish' CompilerSet makeprg=fish\ --no-execute\ % -execute 'CompilerSet errorformat='.escape(fish#errorformat(), ' ') +execute 'CompilerSet errorformat='.escape(fish#errorformat(), ' ') diff --git a/ftdetect/fish.vim b/ftdetect/fish.vim index cbb7d0d..9a462de 100644 --- a/ftdetect/fish.vim +++ b/ftdetect/fish.vim @@ -1,23 +1,13 @@ autocmd BufRead,BufNewFile *.fish setfiletype fish +" Set filetype when using funced. +autocmd BufRead fish_funced.* setfiletype fish + +" Fish histories are YAML documents. +autocmd BufRead,BufNewFile ~/.config/fish/fish_{read_,}history setfiletype yaml + " Detect fish scripts by the shebang line. autocmd BufRead * \ if getline(1) =~# '\v^#!%(\f*/|/usr/bin/env\s*<)fish>' | \ setlocal filetype=fish | \ endif - -" Move cursor to first empty line when using funced. -autocmd BufRead fish_funced_*_*.fish call search('^$') - -" Fish histories are YAML documents. -autocmd BufRead,BufNewFile ~/.config/fish/fish_{read_,}history setfiletype yaml - -" Universal variable storages should not be hand edited. -autocmd BufRead,BufNewFile ~/.config/fish/fishd.* setlocal readonly - -" Mimic `funced` when manually creating functions. -autocmd BufNewFile ~/.config/fish/functions/*.fish - \ call append(0, ['function '.expand('%:t:r'), - \'', - \'end']) | - \ 2 diff --git a/ftplugin/fish.vim b/ftplugin/fish.vim index 85873eb..1ade3f6 100644 --- a/ftplugin/fish.vim +++ b/ftplugin/fish.vim @@ -1,3 +1,11 @@ +if exists('b:did_ftplugin') + finish +end +let b:did_ftplugin = 1 + +let s:save_cpo = &cpo +set cpo&vim + setlocal comments=:# setlocal commentstring=#%s setlocal define=\\v^\\s*function> @@ -5,7 +13,7 @@ setlocal foldexpr=fish#Fold() setlocal formatoptions+=ron1 setlocal formatoptions-=t setlocal include=\\v^\\s*\\.> -setlocal iskeyword=@,48-57,-,_,.,/ +setlocal iskeyword=@,48-57,+,-,_,. setlocal suffixesadd^=.fish " Use the 'j' format option when available. @@ -29,11 +37,31 @@ endif " Use the 'man' wrapper function in fish to include fish's man pages. " Have to use a script for this; 'fish -c man' would make the the man page an " argument to fish instead of man. -execute 'setlocal keywordprg=fish\ '.fnameescape(expand(':p:h:h').'/bin/man.fish') +" execute 'setlocal keywordprg=fish\ '.fnameescape(expand(':p:h:h').'/bin/man.fish') +setlocal keywordprg=:FishHelp -let b:match_words = - \ escape('<%(begin|function|if|switch|while|for)>:', '<>%|)') +let b:match_ignorecase = 0 +if has('patch-7.3.1037') + let s:if = '%(else\s\+)\@15:::' + \, '<>%|)') +let b:match_skip = 's:comment\|string\|deref' let b:endwise_addition = 'end' let b:endwise_words = 'begin,function,if,switch,while,for' -let b:endwise_syngroups = 'fishKeyword,fishConditional,fishRepeat' +let b:endwise_syngroups = 'fishBlock,fishFunction,fishConditional,fishRepeat' + +let b:undo_ftplugin = " + \ setlocal comments< commentstring< define< foldexpr< formatoptions< + \|setlocal include< iskeyword< suffixesadd< + \|setlocal formatexpr< omnifunc< path< keywordprg< + \|unlet! b:match_words b:endwise_addition b:endwise_words b:endwise_syngroups + \" + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/indent/fish.vim b/indent/fish.vim index d1ef6be..5e780b7 100644 --- a/indent/fish.vim +++ b/indent/fish.vim @@ -1,2 +1,3 @@ setlocal indentexpr=fish#Indent() +setlocal indentkeys=!^F,o,O setlocal indentkeys+==end,=else,=case diff --git a/plugin/fish.vim b/plugin/fish.vim new file mode 100644 index 0000000..4283117 --- /dev/null +++ b/plugin/fish.vim @@ -0,0 +1,21 @@ +if get(g:, 'loaded_fish', 0) + finish +endif +let loaded_fish = 1 + +" Universal variable storages should not be hand edited. +autocmd BufRead,BufNewFile ~/.config/fish/fishd.*,~/.config/fish/fish_variables + \ setlocal readonly + +" When using funced: +" - Reindent (because funced adds a tab on the first empty line and the user may +" have set expandtab). +" - Move the cursor to the first empty line. +autocmd BufRead fish_funced.*,fish_funced_*_*.fish,/tmp/fish.*/*.fish + \ retab | call search('^\s*\zs$') + +" Mimic `funced` when manually creating functions. +autocmd BufNewFile ~/.config/fish/functions/*.fish + \ call setline(1, ['function '.expand('%:t:r'), '', 'end']) | 2 + +command! -nargs=? FishHelp call fish#Help() diff --git a/syntax/fish.vim b/syntax/fish.vim index 18eccce..c36a4c4 100644 --- a/syntax/fish.vim +++ b/syntax/fish.vim @@ -3,35 +3,91 @@ if exists('b:current_syntax') endif syntax case match +syntax iskeyword @,48-57,-,_,.,/ -syntax keyword fishKeyword begin function end +" https://en.wikipedia.org/wiki/List_of_Unix_commands +syntax keyword fishUnixCommand admin alias ar asa at awk basename batch bc bg + \ cc c99 cal cat cd cflow chgrp chmod chown cksum cmp comm command compress + \ cp crontab csplit ctags cut cxref date dd delta df diff dirname du echo + \ ed env ex expand expr false fc fg file find fold fort77 fuser gencat get + \ getconf getopts grep hash head iconv id ipcrm ipcs jobs join kill lex + \ less link ln locale localedef logger logname lp ls m4 mailx make man mesg + \ mkdir mkfifo more mv newgrp nice nl nm nohup od paste patch pathchk pax + \ pr printf prs ps pwd qalter qdel qhold qmove qmsg qrerun qrls qselect + \ qsig qstat qsub read renice rm rmdel rmdir sact sccs sed sh sleep sort + \ split strings strip stty tabs tail talk tee test time touch tput tr true + \ tsort tty type ulimit umask unalias uname uncompress unexpand unget uniq + \ unlink uucp uudecode uuencode uustat uux val vi wait wc what who + \ write xargs yacc zcat + +syntax cluster fishKeyword contains=fishBlock,fishFunction,fishConditional, + \ fishRepeat,fishLabel,fishControl,fishOperator,fishBoolean,fishCommand +syntax keyword fishBlock begin end syntax keyword fishConditional if else switch syntax keyword fishRepeat while for in syntax keyword fishLabel case +syntax keyword fishControl return break continue exit +syntax keyword fishOperator and or not +syntax keyword fishBoolean true false + +" http://fishshell.com/docs/current/commands.html +syntax keyword fishCommand abbr alias argparse bg bind block breakpoint + \ builtin cd cdh command commandline complete contains[] count dirh dirs + \ disown echo emit eval exec fg fish fish_breakpoint_prompt fish_config + \ fish_git_prompt fish_hg_prompt fish_indent fish_key_reader + \ fish_mode_prompt fish_opt fish_prompt fish_right_prompt + \ fish_svn_prompt fish_update_completions fish_vcs_prompt funced funcsave + \ functions help history isatty jobs math nextd open popd prevd printf + \ prompt_pwd psub pushd pwd random read realpath set set_color source + \ status suspend test time trap type ulimit umask vared wait +syntax match fishCommand /\v/ + +syntax keyword fishFunction function nextgroup=fishFunctionName skipwhite +syntax match fishFunctionName '[^[:space:]/()-][^[:space:]/()]*' contained + \ contains=fishString,fishDeref + +syntax match fishOperator '[\[\]=*~%&|<>!+-]' +syntax match fishOperator '\.\.' syntax match fishComment /#.*/ -syntax match fishSpecial /\\$/ -syntax match fishIdentifier /\$[[:alnum:]_]\+/ -syntax region fishString start=/'/ skip=/\\'/ end=/'/ -syntax region fishString start=/"/ skip=/\\"/ end=/"/ contains=fishIdentifier -syntax match fishCharacter /\v\\[abefnrtv *?~%#(){}\[\]<>&;"']|\\[xX][0-9a-f]{1,2}|\\o[0-7]{1,2}|\\u[0-9a-f]{1,4}|\\U[0-9a-f]{1,8}|\\c[a-z]/ -syntax match fishStatement /\v;\s*\zs\k+>/ -syntax match fishCommandSub /\v\(\s*\zs\k+>/ - -syntax region fishLineContinuation matchgroup=fishStatement - \ start='\v^\s*\zs\k+>' skip='\\$' end='$' - \ contains=fishSpecial,fishIdentifier,fishString,fishCharacter,fishStatement,fishCommandSub,fishComment +syntax match fishSpecial /[\();]/ +syntax match fishSpecial /\\\$/ +syntax match fishOption /\v<[+-][[:alnum:]-_]+>/ +syntax match fishNumber /\v<[+-]=(\d+\.)=\d+>/ + +syntax match fishDeref /\$\+[[:alnum:]_]\+/ nextgroup=fishDerefExtension +syntax region fishDerefExtension matchgroup=fishOperator start=/\[/ end=/\]/ contains=fishDeref,fishNumber,fishOperator contained + +syntax match fishSingleQuoteEscape /\\[\\']/ contained +syntax match fishDoubleQuoteEscape /\\[\\"$\n]/ contained +syntax cluster fishStringEscape contains=fishSingleQuoteEscape,fishDoubleQuoteEscape + +syntax region fishString start=/'/ skip=/\v(\\{2})|(\\)'/ end=/'/ contains=fishSingleQuoteEscape +syntax region fishString start=/"/ skip=/\v(\\{2})|(\\)"/ end=/"/ contains=fishDoubleQuoteEscape,fishDeref,fishDerefExtension +syntax match fishCharacter /\v\\[0abefnrtv *?~%#(){}\[\]<>&;"']|\\[xX][0-9a-f]{1,2}|\\o[0-7]{1,2}|\\u[0-9a-f]{1,4}|\\U[0-9a-f]{1,8}|\\c[a-z]/ +syntax match fishCharacter /\v\\e[a-zA-Z0-9]/ highlight default link fishKeyword Keyword +highlight default link fishBlock fishKeyword +highlight default link fishFunction fishKeyword highlight default link fishConditional Conditional highlight default link fishRepeat Repeat highlight default link fishLabel Label +highlight default link fishCommand Keyword +highlight default link fishUnixCommand Keyword +highlight default link fishFunctionName Function highlight default link fishComment Comment +highlight default link fishOperator Operator highlight default link fishSpecial Special -highlight default link fishIdentifier Identifier +highlight default link fishDeref PreProc highlight default link fishString String +highlight default link fishSingleQuoteEscape Special +highlight default link fishDoubleQuoteEscape Special +highlight default link fishNumber Number highlight default link fishCharacter Character -highlight default link fishStatement Statement -highlight default link fishCommandSub fishStatement +highlight default link fishOption Constant +highlight default link fishBoolean Boolean +highlight default link fishControl Exception let b:current_syntax = 'fish'