xref: /vim-8.2.3635/runtime/indent/clojure.vim (revision 56994d21)
1" Vim indent file
2" Language:           Clojure
3" Maintainer:         Alex Vear <[email protected]>
4" Former Maintainers: Sung Pae <[email protected]>
5"                     Meikel Brandmeyer <[email protected]>
6" URL:                https://github.com/clojure-vim/clojure.vim
7" License:            Vim (see :h license)
8" Last Change:        2021-02-13
9
10if exists("b:did_indent")
11	finish
12endif
13let b:did_indent = 1
14
15let s:save_cpo = &cpo
16set cpo&vim
17
18let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<'
19
20setlocal noautoindent nosmartindent
21setlocal softtabstop=2 shiftwidth=2 expandtab
22setlocal indentkeys=!,o,O
23
24if exists("*searchpairpos")
25
26	if !exists('g:clojure_maxlines')
27		let g:clojure_maxlines = 100
28	endif
29
30	if !exists('g:clojure_fuzzy_indent')
31		let g:clojure_fuzzy_indent = 1
32	endif
33
34	if !exists('g:clojure_fuzzy_indent_patterns')
35		let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let']
36	endif
37
38	if !exists('g:clojure_fuzzy_indent_blacklist')
39		let g:clojure_fuzzy_indent_blacklist = ['-fn$', '\v^with-%(meta|out-str|loading-context)$']
40	endif
41
42	if !exists('g:clojure_special_indent_words')
43		let g:clojure_special_indent_words = 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn'
44	endif
45
46	if !exists('g:clojure_align_multiline_strings')
47		let g:clojure_align_multiline_strings = 0
48	endif
49
50	if !exists('g:clojure_align_subforms')
51		let g:clojure_align_subforms = 0
52	endif
53
54	function! s:syn_id_name()
55		return synIDattr(synID(line("."), col("."), 0), "name")
56	endfunction
57
58	function! s:ignored_region()
59		return s:syn_id_name() =~? '\vstring|regex|comment|character'
60	endfunction
61
62	function! s:current_char()
63		return getline('.')[col('.')-1]
64	endfunction
65
66	function! s:current_word()
67		return getline('.')[col('.')-1 : searchpos('\v>', 'n', line('.'))[1]-2]
68	endfunction
69
70	function! s:is_paren()
71		return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region()
72	endfunction
73
74	" Returns 1 if string matches a pattern in 'patterns', which may be a
75	" list of patterns, or a comma-delimited string of implicitly anchored
76	" patterns.
77	function! s:match_one(patterns, string)
78		let list = type(a:patterns) == type([])
79		           \ ? a:patterns
80		           \ : map(split(a:patterns, ','), '"^" . v:val . "$"')
81		for pat in list
82			if a:string =~# pat | return 1 | endif
83		endfor
84	endfunction
85
86	function! s:match_pairs(open, close, stopat)
87		" Stop only on vector and map [ resp. {. Ignore the ones in strings and
88		" comments.
89		if a:stopat == 0 && g:clojure_maxlines > 0
90			let stopat = max([line(".") - g:clojure_maxlines, 0])
91		else
92			let stopat = a:stopat
93		endif
94
95		let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:is_paren()", stopat)
96		return [pos[0], col(pos)]
97	endfunction
98
99	function! s:clojure_check_for_string_worker()
100		" Check whether there is the last character of the previous line is
101		" highlighted as a string. If so, we check whether it's a ". In this
102		" case we have to check also the previous character. The " might be the
103		" closing one. In case the we are still in the string, we search for the
104		" opening ". If this is not found we take the indent of the line.
105		let nb = prevnonblank(v:lnum - 1)
106
107		if nb == 0
108			return -1
109		endif
110
111		call cursor(nb, 0)
112		call cursor(0, col("$") - 1)
113		if s:syn_id_name() !~? "string"
114			return -1
115		endif
116
117		" This will not work for a " in the first column...
118		if s:current_char() == '"'
119			call cursor(0, col("$") - 2)
120			if s:syn_id_name() !~? "string"
121				return -1
122			endif
123			if s:current_char() != '\'
124				return -1
125			endif
126			call cursor(0, col("$") - 1)
127		endif
128
129		let p = searchpos('\(^\|[^\\]\)\zs"', 'bW')
130
131		if p != [0, 0]
132			return p[1] - 1
133		endif
134
135		return indent(".")
136	endfunction
137
138	function! s:check_for_string()
139		let pos = getpos('.')
140		try
141			let val = s:clojure_check_for_string_worker()
142		finally
143			call setpos('.', pos)
144		endtry
145		return val
146	endfunction
147
148	function! s:strip_namespace_and_macro_chars(word)
149		return substitute(a:word, "\\v%(.*/|[#'`~@^,]*)(.*)", '\1', '')
150	endfunction
151
152	function! s:clojure_is_method_special_case_worker(position)
153		" Find the next enclosing form.
154		call search('\S', 'Wb')
155
156		" Special case: we are at a '(('.
157		if s:current_char() == '('
158			return 0
159		endif
160		call cursor(a:position)
161
162		let next_paren = s:match_pairs('(', ')', 0)
163
164		" Special case: we are now at toplevel.
165		if next_paren == [0, 0]
166			return 0
167		endif
168		call cursor(next_paren)
169
170		call search('\S', 'W')
171		let w = s:strip_namespace_and_macro_chars(s:current_word())
172
173		if g:clojure_special_indent_words =~# '\V\<' . w . '\>'
174
175			" `letfn` is a special-special-case.
176			if w ==# 'letfn'
177				" Earlier code left the cursor at:
178				"     (letfn [...] ...)
179				"      ^
180
181				" Search and get coordinates of first `[`
182				"     (letfn [...] ...)
183				"            ^
184				call search('\[', 'W')
185				let pos = getcurpos()
186				let letfn_bracket = [pos[1], pos[2]]
187
188				" Move cursor to start of the form this function was
189				" initially called on.  Grab the coordinates of the
190				" closest outer `[`.
191				call cursor(a:position)
192				let outer_bracket = s:match_pairs('\[', '\]', 0)
193
194				" If the located square brackets are not the same,
195				" don't use special-case formatting.
196				if outer_bracket != letfn_bracket
197					return 0
198				endif
199			endif
200
201			return 1
202		endif
203
204		return 0
205	endfunction
206
207	function! s:is_method_special_case(position)
208		let pos = getpos('.')
209		try
210			let val = s:clojure_is_method_special_case_worker(a:position)
211		finally
212			call setpos('.', pos)
213		endtry
214		return val
215	endfunction
216
217	" Check if form is a reader conditional, that is, it is prefixed by #?
218	" or @#?
219	function! s:is_reader_conditional_special_case(position)
220		return getline(a:position[0])[a:position[1] - 3 : a:position[1] - 2] == "#?"
221	endfunction
222
223	" Returns 1 for opening brackets, -1 for _anything else_.
224	function! s:bracket_type(char)
225		return stridx('([{', a:char) > -1 ? 1 : -1
226	endfunction
227
228	" Returns: [opening-bracket-lnum, indent]
229	function! s:clojure_indent_pos()
230		" Get rid of special case.
231		if line(".") == 1
232			return [0, 0]
233		endif
234
235		" We have to apply some heuristics here to figure out, whether to use
236		" normal lisp indenting or not.
237		let i = s:check_for_string()
238		if i > -1
239			return [0, i + !!g:clojure_align_multiline_strings]
240		endif
241
242		call cursor(0, 1)
243
244		" Find the next enclosing [ or {. We can limit the second search
245		" to the line, where the [ was found. If no [ was there this is
246		" zero and we search for an enclosing {.
247		let paren = s:match_pairs('(', ')', 0)
248		let bracket = s:match_pairs('\[', '\]', paren[0])
249		let curly = s:match_pairs('{', '}', bracket[0])
250
251		" In case the curly brace is on a line later then the [ or - in
252		" case they are on the same line - in a higher column, we take the
253		" curly indent.
254		if curly[0] > bracket[0] || curly[1] > bracket[1]
255			if curly[0] > paren[0] || curly[1] > paren[1]
256				return curly
257			endif
258		endif
259
260		" If the curly was not chosen, we take the bracket indent - if
261		" there was one.
262		if bracket[0] > paren[0] || bracket[1] > paren[1]
263			return bracket
264		endif
265
266		" There are neither { nor [ nor (, ie. we are at the toplevel.
267		if paren == [0, 0]
268			return paren
269		endif
270
271		" Now we have to reimplement lispindent. This is surprisingly easy, as
272		" soon as one has access to syntax items.
273		"
274		" - Check whether we are in a special position after a word in
275		"   g:clojure_special_indent_words. These are special cases.
276		" - Get the next keyword after the (.
277		" - If its first character is also a (, we have another sexp and align
278		"   one column to the right of the unmatched (.
279		" - In case it is in lispwords, we indent the next line to the column of
280		"   the ( + sw.
281		" - If not, we check whether it is last word in the line. In that case
282		"   we again use ( + sw for indent.
283		" - In any other case we use the column of the end of the word + 2.
284		call cursor(paren)
285
286		if s:is_method_special_case(paren)
287			return [paren[0], paren[1] + &shiftwidth - 1]
288		endif
289
290		if s:is_reader_conditional_special_case(paren)
291			return paren
292		endif
293
294		" In case we are at the last character, we use the paren position.
295		if col("$") - 1 == paren[1]
296			return paren
297		endif
298
299		" In case after the paren is a whitespace, we search for the next word.
300		call cursor(0, col('.') + 1)
301		if s:current_char() == ' '
302			call search('\v\S', 'W')
303		endif
304
305		" If we moved to another line, there is no word after the (. We
306		" use the ( position for indent.
307		if line(".") > paren[0]
308			return paren
309		endif
310
311		" We still have to check, whether the keyword starts with a (, [ or {.
312		" In that case we use the ( position for indent.
313		let w = s:current_word()
314		if s:bracket_type(w[0]) == 1
315			return paren
316		endif
317
318		" If the keyword begins with #, check if it is an anonymous
319		" function or set, in which case we indent by the shiftwidth
320		" (minus one if g:clojure_align_subforms = 1), or if it is
321		" ignored, in which case we use the ( position for indent.
322		if w[0] == "#"
323			" TODO: Handle #=() and other rare reader invocations?
324			if w[1] == '(' || w[1] == '{'
325				return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
326			elseif w[1] == '_'
327				return paren
328			endif
329		endif
330
331		" Test words without namespace qualifiers and leading reader macro
332		" metacharacters.
333		"
334		" e.g. clojure.core/defn and #'defn should both indent like defn.
335		let ww = s:strip_namespace_and_macro_chars(w)
336
337		if &lispwords =~# '\V\<' . ww . '\>'
338			return [paren[0], paren[1] + &shiftwidth - 1]
339		endif
340
341		if g:clojure_fuzzy_indent
342			\ && !s:match_one(g:clojure_fuzzy_indent_blacklist, ww)
343			\ && s:match_one(g:clojure_fuzzy_indent_patterns, ww)
344			return [paren[0], paren[1] + &shiftwidth - 1]
345		endif
346
347		call search('\v\_s', 'cW')
348		call search('\v\S', 'W')
349		if paren[0] < line(".")
350			return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
351		endif
352
353		call search('\v\S', 'bW')
354		return [line('.'), col('.') + 1]
355	endfunction
356
357	function! GetClojureIndent()
358		let lnum = line('.')
359		let orig_lnum = lnum
360		let orig_col = col('.')
361		let [opening_lnum, indent] = s:clojure_indent_pos()
362
363		" Account for multibyte characters
364		if opening_lnum > 0
365			let indent -= indent - virtcol([opening_lnum, indent])
366		endif
367
368		" Return if there are no previous lines to inherit from
369		if opening_lnum < 1 || opening_lnum >= lnum - 1
370			call cursor(orig_lnum, orig_col)
371			return indent
372		endif
373
374		let bracket_count = 0
375
376		" Take the indent of the first previous non-white line that is
377		" at the same sexp level. cf. src/misc1.c:get_lisp_indent()
378		while 1
379			let lnum = prevnonblank(lnum - 1)
380			let col = 1
381
382			if lnum <= opening_lnum
383				break
384			endif
385
386			call cursor(lnum, col)
387
388			" Handle bracket counting edge case
389			if s:is_paren()
390				let bracket_count += s:bracket_type(s:current_char())
391			endif
392
393			while 1
394				if search('\v[(\[{}\])]', '', lnum) < 1
395					break
396				elseif !s:ignored_region()
397					let bracket_count += s:bracket_type(s:current_char())
398				endif
399			endwhile
400
401			if bracket_count == 0
402				" Check if this is part of a multiline string
403				call cursor(lnum, 1)
404				if s:syn_id_name() !~? '\vstring|regex'
405					call cursor(orig_lnum, orig_col)
406					return indent(lnum)
407				endif
408			endif
409		endwhile
410
411		call cursor(orig_lnum, orig_col)
412		return indent
413	endfunction
414
415	setlocal indentexpr=GetClojureIndent()
416
417else
418
419	" In case we have searchpairpos not available we fall back to
420	" normal lisp indenting.
421	setlocal indentexpr=
422	setlocal lisp
423	let b:undo_indent .= '| setlocal lisp<'
424
425endif
426
427let &cpo = s:save_cpo
428unlet! s:save_cpo
429
430" vim:sts=8:sw=8:ts=8:noet
431