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