neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

falcon.vim (14171B)


      1 " Vim indent file
      2 " Language: Falcon
      3 " Maintainer: Steven Oliver <oliver.steven@gmail.com>
      4 " Website: https://steveno@github.com/steveno/falconpl-vim.git
      5 " Credits: This is, to a great extent, a copy n' paste of ruby.vim.
      6 "		2022 April: b:undo_indent added by Doug Kearns
      7 
      8 " 1. Setup {{{1
      9 " ============
     10 
     11 " Only load this indent file when no other was loaded.
     12 if exists("b:did_indent")
     13    finish
     14 endif
     15 let b:did_indent = 1
     16 
     17 setlocal nosmartindent
     18 
     19 " Setup indent function and when to use it
     20 setlocal indentexpr=FalconGetIndent(v:lnum)
     21 setlocal indentkeys=0{,0},0),0],!^F,o,O,e
     22 setlocal indentkeys+==~case,=~catch,=~default,=~elif,=~else,=~end,=~\"
     23 
     24 let b:undo_indent = "setl inde< indk< si<"
     25 
     26 " Define the appropriate indent function but only once
     27 if exists("*FalconGetIndent")
     28    finish
     29 endif
     30 
     31 let s:cpo_save = &cpo
     32 set cpo&vim
     33 
     34 " 2. Variables {{{1
     35 " ============
     36 
     37 " Regex of syntax group names that are strings AND comments
     38 let s:syng_strcom = '\<falcon\%(String\|StringEscape\|Comment\)\>'
     39 
     40 " Regex of syntax group names that are strings
     41 let s:syng_string = '\<falcon\%(String\|StringEscape\)\>'
     42 
     43 " Regex that defines blocks.
     44 "
     45 " Note that there's a slight problem with this regex and s:continuation_regex.
     46 " Code like this will be matched by both:
     47 "
     48 "   method_call do |(a, b)|
     49 "
     50 " The reason is that the pipe matches a hanging "|" operator.
     51 "
     52 let s:block_regex =
     53      \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|\s*(*\s*\%([*@&]\=\h\w*,\=\s*\)\%(,\s*(*\s*[*@&]\=\h\w*\s*)*\s*\)*|\)\=\s*\%(#.*\)\=$'
     54 
     55 let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
     56 
     57 " Regex that defines continuation lines.
     58 " TODO: this needs to deal with if ...: and so on
     59 let s:continuation_regex =
     60      \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
     61 
     62 " Regex that defines bracket continuations
     63 let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
     64 
     65 " Regex that defines continuation lines, not including (, {, or [.
     66 let s:non_bracket_continuation_regex = '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
     67 
     68 " Keywords to indent on
     69 let s:falcon_indent_keywords = '^\s*\(case\|catch\|class\|enum\|default\|elif\|else' .
     70    \ '\|for\|function\|if.*"[^"]*:.*"\|if \(\(:\)\@!.\)*$\|loop\|object\|select' .
     71    \ '\|switch\|try\|while\|\w*\s*=\s*\w*([$\)'
     72 
     73 " Keywords to deindent on
     74 let s:falcon_deindent_keywords = '^\s*\(case\|catch\|default\|elif\|else\|end\)'
     75 
     76 " 3. Functions {{{1
     77 " ============
     78 
     79 " Check if the character at lnum:col is inside a string, comment, or is ascii.
     80 function s:IsInStringOrComment(lnum, col)
     81    return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom
     82 endfunction
     83 
     84 " Check if the character at lnum:col is inside a string.
     85 function s:IsInString(lnum, col)
     86    return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_string
     87 endfunction
     88 
     89 " Check if the character at lnum:col is inside a string delimiter
     90 function s:IsInStringDelimiter(lnum, col)
     91    return synIDattr(synID(a:lnum, a:col, 1), 'name') == 'falconStringDelimiter'
     92 endfunction
     93 
     94 " Find line above 'lnum' that isn't empty, in a comment, or in a string.
     95 function s:PrevNonBlankNonString(lnum)
     96    let in_block = 0
     97    let lnum = prevnonblank(a:lnum)
     98    while lnum > 0
     99 " Go in and out of blocks comments as necessary.
    100 " If the line isn't empty (with opt. comment) or in a string, end search.
    101 let line = getline(lnum)
    102 if line =~ '^=begin'
    103     if in_block
    104 	let in_block = 0
    105     else
    106 	break
    107     endif
    108 elseif !in_block && line =~ '^=end'
    109     let in_block = 1
    110 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
    111 	    \ && s:IsInStringOrComment(lnum, strlen(line)))
    112     break
    113 endif
    114 let lnum = prevnonblank(lnum - 1)
    115    endwhile
    116    return lnum
    117 endfunction
    118 
    119 " Find line above 'lnum' that started the continuation 'lnum' may be part of.
    120 function s:GetMSL(lnum)
    121    " Start on the line we're at and use its indent.
    122    let msl = a:lnum
    123    let msl_body = getline(msl)
    124    let lnum = s:PrevNonBlankNonString(a:lnum - 1)
    125    while lnum > 0
    126 " If we have a continuation line, or we're in a string, use line as MSL.
    127 " Otherwise, terminate search as we have found our MSL already.
    128 let line = getline(lnum)
    129 
    130 if s:Match(line, s:non_bracket_continuation_regex) &&
    131          	\ s:Match(msl, s:non_bracket_continuation_regex)
    132     " If the current line is a non-bracket continuation and so is the
    133     " previous one, keep its indent and continue looking for an MSL.
    134     "    
    135     " Example:
    136     "   method_call one,
    137     "       two,
    138     "           three
    139     "           
    140     let msl = lnum
    141 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
    142 	    \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
    143     " If the current line is a bracket continuation or a block-starter, but
    144     " the previous is a non-bracket one, respect the previous' indentation,
    145     " and stop here.
    146     " 
    147     " Example:
    148     "   method_call one,
    149     "       two {
    150     "           three
    151     "
    152     return lnum
    153 elseif s:Match(lnum, s:bracket_continuation_regex) &&
    154 	    \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
    155     " If both lines are bracket continuations (the current may also be a
    156     " block-starter), use the current one's and stop here
    157     "
    158     " Example:
    159     "   method_call(
    160     "       other_method_call(
    161     "             foo
    162     return msl
    163 elseif s:Match(lnum, s:block_regex) &&
    164 	    \ !s:Match(msl, s:continuation_regex) &&
    165 	    \ !s:Match(msl, s:block_continuation_regex)
    166     " If the previous line is a block-starter and the current one is
    167     " mostly ordinary, use the current one as the MSL.
    168     " 
    169     " Example:
    170     "   method_call do
    171     "       something
    172     "           something_else
    173     return msl
    174 else
    175     let col = match(line, s:continuation_regex) + 1
    176     if (col > 0 && !s:IsInStringOrComment(lnum, col))
    177 		\ || s:IsInString(lnum, strlen(line))
    178 	let msl = lnum
    179     else
    180 	break
    181     endif
    182 endif
    183 
    184 let msl_body = getline(msl)
    185 let lnum = s:PrevNonBlankNonString(lnum - 1)
    186    endwhile
    187    return msl
    188 endfunction
    189 
    190 " Check if line 'lnum' has more opening brackets than closing ones.
    191 function s:ExtraBrackets(lnum)
    192    let opening = {'parentheses': [], 'braces': [], 'brackets': []}
    193    let closing = {'parentheses': [], 'braces': [], 'brackets': []}
    194 
    195    let line = getline(a:lnum)
    196    let pos  = match(line, '[][(){}]', 0)
    197 
    198    " Save any encountered opening brackets, and remove them once a matching
    199    " closing one has been found. If a closing bracket shows up that doesn't
    200    " close anything, save it for later.
    201    while pos != -1
    202 if !s:IsInStringOrComment(a:lnum, pos + 1)
    203     if line[pos] == '('
    204 	call add(opening.parentheses, {'type': '(', 'pos': pos})
    205     elseif line[pos] == ')'
    206 	if empty(opening.parentheses)
    207 	    call add(closing.parentheses, {'type': ')', 'pos': pos})
    208 	else
    209 	    let opening.parentheses = opening.parentheses[0:-2]
    210 	endif
    211     elseif line[pos] == '{'
    212 	call add(opening.braces, {'type': '{', 'pos': pos})
    213     elseif line[pos] == '}'
    214 	if empty(opening.braces)
    215 	    call add(closing.braces, {'type': '}', 'pos': pos})
    216 	else
    217 	    let opening.braces = opening.braces[0:-2]
    218 	endif
    219     elseif line[pos] == '['
    220 	call add(opening.brackets, {'type': '[', 'pos': pos})
    221     elseif line[pos] == ']'
    222 	if empty(opening.brackets)
    223 	    call add(closing.brackets, {'type': ']', 'pos': pos})
    224 	else
    225 	    let opening.brackets = opening.brackets[0:-2]
    226 	endif
    227     endif
    228 endif
    229 
    230 let pos = match(line, '[][(){}]', pos + 1)
    231    endwhile
    232 
    233    " Find the rightmost brackets, since they're the ones that are important in
    234    " both opening and closing cases
    235    let rightmost_opening = {'type': '(', 'pos': -1}
    236    let rightmost_closing = {'type': ')', 'pos': -1}
    237 
    238    for opening in opening.parentheses + opening.braces + opening.brackets
    239 if opening.pos > rightmost_opening.pos
    240     let rightmost_opening = opening
    241 endif
    242    endfor
    243 
    244    for closing in closing.parentheses + closing.braces + closing.brackets
    245 if closing.pos > rightmost_closing.pos
    246     let rightmost_closing = closing
    247 endif
    248    endfor
    249 
    250    return [rightmost_opening, rightmost_closing]
    251 endfunction
    252 
    253 function s:Match(lnum, regex)
    254    let col = match(getline(a:lnum), '\C'.a:regex) + 1
    255    return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0
    256 endfunction
    257 
    258 function s:MatchLast(lnum, regex)
    259    let line = getline(a:lnum)
    260    let col = match(line, '.*\zs' . a:regex)
    261    while col != -1 && s:IsInStringOrComment(a:lnum, col)
    262 let line = strpart(line, 0, col)
    263 let col = match(line, '.*' . a:regex)
    264    endwhile
    265    return col + 1
    266 endfunction
    267 
    268 " 4. FalconGetIndent Routine {{{1
    269 " ============
    270 
    271 function FalconGetIndent(...)
    272    " For the current line, use the first argument if given, else v:lnum
    273    let clnum = a:0 ? a:1 : v:lnum
    274 
    275    " Use zero indent at the top of the file
    276    if clnum == 0
    277        return 0
    278    endif
    279 
    280    let line = getline(clnum)
    281    let ind = -1
    282 
    283    " If we got a closing bracket on an empty line, find its match and indent
    284    " according to it.  For parentheses we indent to its column - 1, for the
    285    " others we indent to the containing line's MSL's level.  Return -1 if fail.
    286    let col = matchend(line, '^\s*[]})]')
    287    if col > 0 && !s:IsInStringOrComment(clnum, col)
    288 call cursor(clnum, col)
    289 let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
    290 if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0
    291     if line[col-1]==')' && col('.') != col('$') - 1
    292 	let ind = virtcol('.') - 1
    293     else
    294 	let ind = indent(s:GetMSL(line('.')))
    295     endif
    296 endif
    297 return ind
    298    endif
    299 
    300    " If we have a deindenting keyword, find its match and indent to its level.
    301    " TODO: this is messy
    302    if s:Match(clnum, s:falcon_deindent_keywords)
    303 call cursor(clnum, 1)
    304 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
    305 	    \ s:end_skip_expr) > 0
    306     let msl  = s:GetMSL(line('.'))
    307     let line = getline(line('.'))
    308 
    309     if strpart(line, 0, col('.') - 1) =~ '=\s*$' &&
    310 		\ strpart(line, col('.') - 1, 2) !~ 'do'
    311 	let ind = virtcol('.') - 1
    312     elseif getline(msl) =~ '=\s*\(#.*\)\=$'
    313 	let ind = indent(line('.'))
    314     else
    315 	let ind = indent(msl)
    316     endif
    317 endif
    318 return ind
    319    endif
    320 
    321    " If we are in a multi-line string or line-comment, don't do anything to it.
    322    if s:IsInString(clnum, matchend(line, '^\s*') + 1)
    323 return indent('.')
    324    endif
    325 
    326    " Find a non-blank, non-multi-line string line above the current line.
    327    let lnum = s:PrevNonBlankNonString(clnum - 1)
    328 
    329    " If the line is empty and inside a string, use the previous line.
    330    if line =~ '^\s*$' && lnum != prevnonblank(clnum - 1)
    331 return indent(prevnonblank(clnum))
    332    endif
    333 
    334    " At the start of the file use zero indent.
    335    if lnum == 0
    336 return 0
    337    endif
    338 
    339    " Set up variables for the previous line.
    340    let line = getline(lnum)
    341    let ind = indent(lnum)
    342 
    343    " If the previous line ended with a block opening, add a level of indent.
    344    if s:Match(lnum, s:block_regex)
    345 return indent(s:GetMSL(lnum)) + shiftwidth()
    346    endif
    347 
    348    " If it contained hanging closing brackets, find the rightmost one, find its
    349    " match and indent according to that.
    350    if line =~ '[[({]' || line =~ '[])}]\s*\%(#.*\)\=$'
    351 let [opening, closing] = s:ExtraBrackets(lnum)
    352 
    353 if opening.pos != -1
    354     if opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
    355 	if col('.') + 1 == col('$')
    356 	    return ind + shiftwidth()
    357 	else
    358 	    return virtcol('.')
    359 	endif
    360     else
    361 	let nonspace = matchend(line, '\S', opening.pos + 1) - 1
    362 	return nonspace > 0 ? nonspace : ind + shiftwidth()
    363     endif
    364 elseif closing.pos != -1
    365     call cursor(lnum, closing.pos + 1)
    366     normal! %
    367 
    368     if s:Match(line('.'), s:falcon_indent_keywords)
    369 	return indent('.') + shiftwidth()
    370     else
    371 	return indent('.')
    372     endif
    373 else
    374     call cursor(clnum, 0)  " FIXME: column was vcol
    375 end
    376    endif
    377 
    378    " If the previous line ended with an "end", match that "end"s beginning's
    379    " indent.
    380    let col = s:Match(lnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
    381    if col > 0
    382 call cursor(lnum, col)
    383 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
    384 	    \ s:end_skip_expr) > 0
    385     let n = line('.')
    386     let ind = indent('.')
    387     let msl = s:GetMSL(n)
    388     if msl != n
    389 	let ind = indent(msl)
    390     end
    391     return ind
    392 endif
    393    end
    394 
    395    let col = s:Match(lnum, s:falcon_indent_keywords)
    396    if col > 0
    397 call cursor(lnum, col)
    398 let ind = virtcol('.') - 1 + shiftwidth()
    399 " TODO: make this better (we need to count them) (or, if a searchpair
    400 " fails, we know that something is lacking an end and thus we indent a
    401 " level
    402 if s:Match(lnum, s:end_end_regex)
    403     let ind = indent('.')
    404 endif
    405 return ind
    406    endif
    407 
    408    " Set up variables to use and search for MSL to the previous line.
    409    let p_lnum = lnum
    410    let lnum = s:GetMSL(lnum)
    411 
    412    " If the previous line wasn't a MSL and is continuation return its indent.
    413    " TODO: the || s:IsInString() thing worries me a bit.
    414    if p_lnum != lnum
    415 if s:Match(p_lnum, s:non_bracket_continuation_regex) || s:IsInString(p_lnum,strlen(line))
    416     return ind
    417 endif
    418    endif
    419 
    420    " Set up more variables, now that we know we wasn't continuation bound.
    421    let line = getline(lnum)
    422    let msl_ind = indent(lnum)
    423 
    424    " If the MSL line had an indenting keyword in it, add a level of indent.
    425    " TODO: this does not take into account contrived things such as
    426    " module Foo; class Bar; end
    427    if s:Match(lnum, s:falcon_indent_keywords)
    428 let ind = msl_ind + shiftwidth()
    429 if s:Match(lnum, s:end_end_regex)
    430     let ind = ind - shiftwidth()
    431 endif
    432 return ind
    433    endif
    434 
    435    " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
    436    " closing bracket, indent one extra level.
    437    if s:Match(lnum, s:non_bracket_continuation_regex) && !s:Match(lnum, '^\s*\([\])}]\|end\)')
    438 if lnum == p_lnum
    439     let ind = msl_ind + shiftwidth()
    440 else
    441     let ind = msl_ind
    442 endif
    443 return ind
    444    endif
    445 
    446  return ind
    447 endfunction
    448 
    449 " }}}1
    450 
    451 let &cpo = s:cpo_save
    452 unlet s:cpo_save
    453 
    454 " vim: set sw=4 sts=4 et tw=80 :