Skip to content

Commit 7d6bdd7

Browse files
committedApr 21, 2023
Begin rewrite of indentation code
The objectives are: 1. Simplify the indentation code; previous implementation has become so complex it is impossible to maintain, 2. Significantly improve performance; previous indentation code was painfully slow, (see issue #6) 3. Maximum configurability; should be configured similarly to cljfmt and make previously impossible things possible (e.g. issue #21). As of this commit, objectives 1 and 2 have been met, but work on objective 3 has not yet begun. There will continue to be further improvements, particularly around performance and the "what if syntax highlighting is disabled?" scenario. These changes will unfortunately be backwards incompatible, but hopefully the improved performance and API will make up for it.
1 parent 06196d8 commit 7d6bdd7

File tree

1 file changed

+119
-413
lines changed

1 file changed

+119
-413
lines changed
 

‎indent/clojure.vim

Lines changed: 119 additions & 413 deletions
Original file line numberDiff line numberDiff line change
@@ -1,438 +1,144 @@
11
" Vim indent file
2-
" Language: Clojure
3-
" Maintainer: Alex Vear <alex@vear.uk>
4-
" Former Maintainers: Sung Pae <self@sungpae.com>
5-
" Meikel Brandmeyer <mb@kotka.de>
6-
" URL: https://github.com/clojure-vim/clojure.vim
7-
" License: Vim (see :h license)
8-
" Last Change: %%RELEASE_DATE%%
2+
" Language: Clojure
3+
" Maintainer: Alex Vear <alex@vear.uk>
4+
" Former Maintainers: Sung Pae <self@sungpae.com>
5+
" Meikel Brandmeyer <mb@kotka.de>
6+
" Last Change: %%RELEASE_DATE%%
7+
" License: Vim (see :h license)
8+
" Repository: https://github.com/clojure-vim/clojure.vim
99

1010
if exists("b:did_indent")
1111
finish
1212
endif
1313
let b:did_indent = 1
1414

15-
let s:save_cpo = &cpo
16-
set cpo&vim
15+
let s:save_cpo = &cpoptions
16+
set cpoptions&vim
1717

1818
let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<'
1919

2020
setlocal noautoindent nosmartindent
2121
setlocal softtabstop=2 shiftwidth=2 expandtab
2222
setlocal indentkeys=!,o,O
2323

24-
if exists("*searchpairpos")
25-
26-
if !exists('g:clojure_maxlines')
27-
let g:clojure_maxlines = 300
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-
if !exists('g:clojure_cljfmt_compat')
55-
let g:clojure_cljfmt_compat = 0
24+
" TODO: ignore 'lisp' and 'lispwords' options (actually, turn them off?)
25+
26+
" TODO: Optional Vim9script implementations of hotspot/bottleneck functions.
27+
" FIXME: fallback case when syntax highlighting is disabled.
28+
29+
" Function to get the indentation of a line.
30+
" function! s:GetIndent(lnum)
31+
" let l = getline(a:lnum)
32+
" return len(l) - len(trim(l, " \t", 1))
33+
" endfunction
34+
35+
function! s:GetSynIdName(line, col)
36+
return synIDattr(synID(a:line, a:col, 0), 'name')
37+
endfunction
38+
39+
function! s:SyntaxMatch(pattern, line, col)
40+
return s:GetSynIdName(a:line, a:col) =~? a:pattern
41+
endfunction
42+
43+
function! s:IgnoredRegion()
44+
return s:SyntaxMatch('\vstring|regex|comment|character', line('.'), col('.'))
45+
endfunction
46+
47+
function! s:NotAStringDelimiter()
48+
return ! s:SyntaxMatch('stringdelimiter', line('.'), col('.'))
49+
endfunction
50+
51+
function! s:IsInString()
52+
return s:SyntaxMatch('string', line('.'), col('.'))
53+
endfunction
54+
55+
function! s:NotARegexpDelimiter()
56+
return ! s:SyntaxMatch('regexpdelimiter', line('.'), col('.'))
57+
endfunction
58+
59+
function! s:IsInRegex()
60+
return s:SyntaxMatch('regex', line('.'), col('.'))
61+
endfunction
62+
63+
function! s:Conf(opt, default)
64+
return get(b:, a:opt, get(g:, a:opt, a:default))
65+
endfunction
66+
67+
function! s:ShouldAlignMultiLineStrings()
68+
return s:Conf('clojure_align_multiline_strings', 0)
69+
endfunction
70+
71+
function! s:ClosestMatch(match1, match2)
72+
let [_, coord1] = a:match1
73+
let [_, coord2] = a:match2
74+
if coord1[0] < coord2[0]
75+
return a:match2
76+
elseif coord1[0] == coord2[0] && coord1[1] < coord2[1]
77+
return a:match2
78+
else
79+
return a:match1
5680
endif
57-
58-
function! s:syn_id_name()
59-
return synIDattr(synID(line("."), col("."), 0), "name")
60-
endfunction
61-
62-
function! s:ignored_region()
63-
return s:syn_id_name() =~? '\vstring|regex|comment|character'
64-
endfunction
65-
66-
function! s:current_char()
67-
return getline('.')[col('.')-1]
68-
endfunction
69-
70-
function! s:current_word()
71-
return getline('.')[col('.')-1 : searchpos('\v>', 'n', line('.'))[1]-2]
72-
endfunction
73-
74-
function! s:is_paren()
75-
return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region()
76-
endfunction
77-
78-
" Returns 1 if string matches a pattern in 'patterns', which should be
79-
" a list of patterns.
80-
function! s:match_one(patterns, string)
81-
for pat in a:patterns
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-
81+
endfunction
82+
83+
" Only need to search up. Never down.
84+
function! s:GetClojureIndent()
85+
let lnum = v:lnum
86+
87+
" Move cursor to the first column of the line we want to indent.
88+
cursor(lnum, 0)
89+
90+
let matches = [
91+
\ ['lst', searchpairpos( '(', '', ')', 'bznW', function('<SID>IgnoredRegion'))],
92+
\ ['vec', searchpairpos('\[', '', '\]', 'bznW', function('<SID>IgnoredRegion'))],
93+
\ ['map', searchpairpos( '{', '', '}', 'bznW', function('<SID>IgnoredRegion'))],
94+
\ ['reg', s:IsInRegex() ? searchpairpos('#\zs"', '', '"', 'bznW', function('<SID>NotARegexpDelimiter')) : [0, 0]],
95+
\ ['str', s:IsInString() ? searchpairpos('"', '', '"', 'bznW', function('<SID>NotAStringDelimiter')) : [0, 0]]
96+
\ ]
97+
echom 'Matches' matches
98+
99+
" Find closest matching higher form.
100+
let [formtype, coord] = reduce(matches, function('<SID>ClosestMatch'), ['top', [0, 0]])
101+
echom 'Match' formtype coord
102+
103+
if formtype == 'top'
104+
" At the top level, no indent.
105+
echom 'At the top level!'
204106
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-
\|| getline(a:position[0])[a:position[1] - 4 : a:position[1] - 2] == "#?@"
222-
endfunction
223-
224-
" Returns 1 for opening brackets, -1 for _anything else_.
225-
function! s:bracket_type(char)
226-
return stridx('([{', a:char) > -1 ? 1 : -1
227-
endfunction
228-
229-
" Returns: [opening-bracket-lnum, indent]
230-
function! s:clojure_indent_pos()
231-
" Get rid of special case.
232-
if line(".") == 1
233-
return [0, 0]
234-
endif
235-
236-
" We have to apply some heuristics here to figure out, whether to use
237-
" normal lisp indenting or not.
238-
let i = s:check_for_string()
239-
if i > -1
240-
return [0, i + !!g:clojure_align_multiline_strings]
241-
endif
242-
243-
call cursor(0, 1)
244-
245-
" Find the next enclosing [ or {. We can limit the second search
246-
" to the line, where the [ was found. If no [ was there this is
247-
" zero and we search for an enclosing {.
248-
let paren = s:match_pairs('(', ')', 0)
249-
let bracket = s:match_pairs('\[', '\]', paren[0])
250-
let curly = s:match_pairs('{', '}', bracket[0])
251-
252-
" In case the curly brace is on a line later then the [ or - in
253-
" case they are on the same line - in a higher column, we take the
254-
" curly indent.
255-
if curly[0] > bracket[0] || curly[1] > bracket[1]
256-
if curly[0] > paren[0] || curly[1] > paren[1]
257-
return curly
258-
endif
259-
endif
260-
261-
" If the curly was not chosen, we take the bracket indent - if
262-
" there was one.
263-
if bracket[0] > paren[0] || bracket[1] > paren[1]
264-
return bracket
265-
endif
266-
267-
" There are neither { nor [ nor (, ie. we are at the toplevel.
268-
if paren == [0, 0]
269-
return paren
270-
endif
271-
272-
" Now we have to reimplement lispindent. This is surprisingly easy, as
273-
" soon as one has access to syntax items.
274-
"
275-
" - Check whether we are in a special position after a word in
276-
" g:clojure_special_indent_words. These are special cases.
277-
" - Get the next keyword after the (.
278-
" - If its first character is also a (, we have another sexp and align
279-
" one column to the right of the unmatched (.
280-
" - In case it is in lispwords, we indent the next line to the column of
281-
" the ( + sw.
282-
" - If not, we check whether it is last word in the line. In that case
283-
" we again use ( + sw for indent.
284-
" - In any other case we use the column of the end of the word + 2.
285-
call cursor(paren)
286-
287-
if s:is_method_special_case(paren)
288-
return [paren[0], paren[1] + &shiftwidth - 1]
289-
endif
290-
291-
if s:is_reader_conditional_special_case(paren)
292-
return paren
293-
endif
294-
295-
" In case we are at the last character, we use the paren position.
296-
if col("$") - 1 == paren[1]
297-
return paren
298-
endif
299-
300-
" In case after the paren is a whitespace, we search for the next word.
301-
call cursor(0, col('.') + 1)
302-
if s:current_char() == ' '
303-
call search('\v\S', 'W')
304-
endif
305-
306-
" If we moved to another line, there is no word after the (. We
307-
" use the ( position for indent.
308-
if line(".") > paren[0]
309-
return paren
310-
endif
311-
312-
" We still have to check, whether the keyword starts with a (, [ or {.
313-
" In that case we use the ( position for indent.
314-
let w = s:current_word()
315-
if s:bracket_type(w[0]) == 1
316-
return paren
317-
endif
318-
319-
" If the keyword begins with #, check if it is an anonymous
320-
" function or set, in which case we indent by the shiftwidth
321-
" (minus one if g:clojure_align_subforms = 1), or if it is
322-
" ignored, in which case we use the ( position for indent.
323-
if w[0] == "#"
324-
" TODO: Handle #=() and other rare reader invocations?
325-
if w[1] == '(' || w[1] == '{'
326-
return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
327-
elseif w[1] == '_'
328-
return paren
329-
elseif w[1] == "'" && g:clojure_cljfmt_compat
330-
return paren
331-
endif
332-
endif
333-
334-
" Paren indent for keywords, symbols and derefs
335-
if g:clojure_cljfmt_compat && w[0] =~# "[:@']"
336-
return paren
337-
endif
338-
339-
" Test words without namespace qualifiers and leading reader macro
340-
" metacharacters.
341-
"
342-
" e.g. clojure.core/defn and #'defn should both indent like defn.
343-
let ww = s:strip_namespace_and_macro_chars(w)
344-
345-
if &lispwords =~# '\V\<' . ww . '\>'
346-
return [paren[0], paren[1] + &shiftwidth - 1]
347-
endif
348-
349-
if g:clojure_fuzzy_indent
350-
\ && !s:match_one(g:clojure_fuzzy_indent_blacklist, ww)
351-
\ && s:match_one(g:clojure_fuzzy_indent_patterns, ww)
352-
return [paren[0], paren[1] + &shiftwidth - 1]
353-
endif
354-
355-
call search('\v\_s', 'cW')
356-
call search('\v\S', 'W')
357-
if paren[0] < line(".")
358-
return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
359-
endif
360-
361-
call search('\v\S', 'bW')
362-
return [line('.'), col('.') + 1]
363-
endfunction
364-
365-
function! GetClojureIndent()
366-
let lnum = line('.')
367-
let orig_lnum = lnum
368-
let orig_col = col('.')
369-
let [opening_lnum, indent] = s:clojure_indent_pos()
370-
371-
" Account for multibyte characters
372-
if opening_lnum > 0
373-
let indent -= indent - virtcol([opening_lnum, indent])
374-
endif
375-
376-
" Return if there are no previous lines to inherit from
377-
if opening_lnum < 1 || opening_lnum >= lnum - 1
378-
call cursor(orig_lnum, orig_col)
379-
return indent
380-
endif
381-
382-
let bracket_count = 0
383-
384-
" Take the indent of the first previous non-white line that is
385-
" at the same sexp level. cf. src/misc1.c:get_lisp_indent()
386-
while 1
387-
let lnum = prevnonblank(lnum - 1)
388-
let col = 1
389-
390-
if lnum <= opening_lnum
391-
break
392-
endif
393-
394-
call cursor(lnum, col)
395-
396-
" Handle bracket counting edge case
397-
if s:is_paren()
398-
let bracket_count += s:bracket_type(s:current_char())
399-
endif
400-
401-
while 1
402-
if search('\v[(\[{}\])]', '', lnum) < 1
403-
break
404-
elseif !s:ignored_region()
405-
let bracket_count += s:bracket_type(s:current_char())
406-
endif
407-
endwhile
408-
409-
if bracket_count == 0
410-
" Check if this is part of a multiline string
411-
call cursor(lnum, 1)
412-
if s:syn_id_name() !~? '\vstring|regex'
413-
call cursor(orig_lnum, orig_col)
414-
return indent(lnum)
415-
endif
416-
endif
417-
endwhile
107+
elseif formtype == 'lst'
108+
echom 'Special format rules!'
109+
" TODO
110+
" Grab text!
111+
echom getline(coord[0], lnum - 1)
112+
" Begin lexing!
113+
return coord[1] + 1
114+
elseif formtype == 'vec' || formtype == 'map'
115+
" Inside a vector, map or set.
116+
return coord[1]
117+
elseif formtype == 'reg'
118+
" Inside a regex.
119+
echom 'Inside a regex!'
120+
return coord[1] - (s:ShouldAlignMultiLineStrings() ? 0 : 2)
121+
elseif formtype == 'str'
122+
" Inside a string.
123+
echom 'Inside a string!'
124+
return coord[1] - (s:ShouldAlignMultiLineStrings() ? 0 : 1)
125+
endif
418126

419-
call cursor(orig_lnum, orig_col)
420-
return indent
421-
endfunction
127+
return 2
128+
endfunction
422129

423-
setlocal indentexpr=GetClojureIndent()
424130

425-
else
131+
setlocal indentexpr=s:GetClojureIndent()
426132

427-
" In case we have searchpairpos not available we fall back to
428-
" normal lisp indenting.
429-
setlocal indentexpr=
430-
setlocal lisp
431-
let b:undo_indent .= '| setlocal lisp<'
432133

433-
endif
134+
" TODO: if exists("*searchpairpos")
135+
" In case we have searchpairpos not available we fall back to normal lisp
136+
" indenting.
137+
"setlocal indentexpr=
138+
"setlocal lisp
139+
"let b:undo_indent .= '| setlocal lisp<'
434140

435-
let &cpo = s:save_cpo
141+
let &cpoptions = s:save_cpo
436142
unlet! s:save_cpo
437143

438144
" vim:sts=8:sw=8:ts=8:noet

0 commit comments

Comments
 (0)
Please sign in to comment.