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