@@ -21,24 +21,86 @@ setlocal noautoindent nosmartindent nolisp
2121setlocal softtabstop = 2 shiftwidth = 2 expandtab
2222setlocal indentkeys = ! ,o ,O
2323
24- function ! s: GetSynIdName (line , col )
25- return synIDattr (synID (a: line , a: col , 0 ), ' name' )
24+ " Returns true if char_idx is preceded by an odd number of backslashes.
25+ function ! s: IsEscaped (line_str, char_idx)
26+ let ln = a:line_str[: a:char_idx - 1]
27+ return (strlen (ln ) - strlen(trim(ln, '\', 2))) % 2
2628endfunction
2729
28- function ! s: SyntaxMatch (pattern, line , col )
29- return s: GetSynIdName (a: line , a: col ) = ~? a: pattern
30- endfunction
30+ let s: pairs = {' (' : ' )' , ' [' : ' ]' , ' {' : ' }' }
31+
32+ " TODO: Maybe write a Vim9script version of this?
33+ " Repeatedly search for tokens on the given line in reverse order building up
34+ " a list of tokens and their positions. Ignores escaped tokens.
35+ function ! s: AnalyseLine (line_num)
36+ let tokens = []
37+ let ln = getline(a:line_num)
38+
39+ while 1
40+ " Due to legacy Vimscript being painfully slow, we literally
41+ " have to move the cursor and perform searches which is
42+ " ironically faster than for looping by character.
43+ let token = searchpos (' [()\[\]{};"]' , ' bW' , a: line_num )
44+
45+ if token == [0 , 0 ] | break | endif
46+ let t_idx = token[1 ] - 1
47+ if s: IsEscaped (ln , t_idx) | continue | endif
48+ let t_char = ln [t_idx]
49+
50+ if t_char == # ' ;'
51+ " Comment found, reset the token list for this line.
52+ tokens = []
53+ elseif t_char = ~# ' [()\[\]{}"]'
54+ " Add token to the list.
55+ call add (tokens, [t_char, token])
56+ endif
57+ endwhile
3158
32- function ! s: IgnoredRegion ()
33- return s: SyntaxMatch (' \%(string\|regex\|comment\|character\)' , line (' .' ), col (' .' ))
59+ return tokens
3460endfunction
3561
36- function ! s: NotStringDelimiter ()
37- return ! s: SyntaxMatch (' stringdelimiter' , line (' .' ), col (' .' ))
38- endfunction
62+ " This should also be capable of figuring out if we're in a multi-line string
63+ " or regex.
64+ function ! s: InverseRead (lnum)
65+ let lnum = a: lnum - 1
66+ let tokens = []
67+
68+ while lnum > 0
69+ call cursor (lnum + 1 , 1 )
70+ let line_tokens = s: AnalyseLine (lnum)
71+
72+ " let should_ignore = empty(a:tokens) ? 0 : (a:tokens[-1][0] ==# '"')
73+
74+ " Reduce "tokens" and "line_tokens".
75+ for t in line_tokens
76+ " TODO: attempt early termination.
77+ if empty (tokens)
78+ call add (tokens, t )
79+ elseif t [0 ] == # ' "' && tokens[-1 ][0 ] == # ' "'
80+ " TODO: track original start and ignore values
81+ " inside strings.
82+ call remove (tokens, -1 )
83+ elseif get (s: pairs , t [0 ], ' ' ) == # tokens[-1 ][0 ]
84+ " Matching pair: drop the last item in tokens.
85+ call remove (tokens, -1 )
86+ else
87+ " No match: append to token list.
88+ call add (tokens, t )
89+ endif
90+ endfor
91+
92+ " echom 'Pass' lnum tokens
93+
94+ if ! empty (tokens) && has_key (s: pairs , tokens[0 ][0 ])
95+ " TODO: on string match, check if string or regex.
96+ " echom 'Match!' tokens[0]
97+ return tokens[0 ]
98+ endif
99+
100+ let lnum -= 1
101+ endwhile
39102
40- function ! s: NotRegexpDelimiter ()
41- return ! s: SyntaxMatch (' regexpdelimiter' , line (' .' ), col (' .' ))
103+ return [' ^' , [0 , 0 ]] " Default to top-level.
42104endfunction
43105
44106function ! s: Conf (opt , default)
@@ -51,7 +113,7 @@ function! s:EqualsOperatorInEffect()
51113 return v: operator == # ' =' && state (' o' ) == # ' o'
52114endfunction
53115
54- function ! s: GetStringIndent (delim_pos, regex )
116+ function ! s: GetStringIndent (delim_pos, is_regex )
55117 " Mimic multi-line string indentation behaviour in VS Code and Emacs.
56118 let m = mode ()
57119 if m == # ' i' || (m == # ' n' && ! s: EqualsOperatorInEffect ())
@@ -62,7 +124,7 @@ function! s:GetStringIndent(delim_pos, regex)
62124 " 1: Indent in alignment with string start delimiter.
63125 if alignment == -1 | return 0
64126 elseif alignment == 1 | return a: delim_pos [1 ]
65- else | return a: delim_pos [1 ] - (a: regex ? 2 : 1 )
127+ else | return a: delim_pos [1 ] - (a: is_regex ? 2 : 1 )
66128 endif
67129 else
68130 return -1 " Keep existing indent.
@@ -71,97 +133,34 @@ endfunction
71133
72134function ! s: GetListIndent (delim_pos)
73135 " TODO Begin analysis and apply rules!
136+ " let lns = getline(delim_pos[0], v:lnum - 1)
74137 let ln1 = getline (delim_pos[0 ])
75138 let sym = get (split (ln1[delim_pos[1 ]:], ' [[:space:],;()\[\]{}@\\"^~`]' , 1 ), 0 , -1 )
76139 if sym != -1 && ! empty (sym) && match (sym, ' ^[0-9:]' ) == -1
77140 " TODO: align indentation.
141+ " TODO: lookup rules.
78142 return delim_pos[1 ] + 1 " 2 space indentation
79143 endif
80144
81145 " TODO: switch between 1 vs 2 space indentation.
82146 return delim_pos[1 ] " 1 space indentation
83147endfunction
84148
85- " Wrapper around "searchpairpos" that will automatically set "s:best_match" to
86- " the closest pair match and optimises the "stopline" value for later
87- " searches. This results in a significant performance gain by reducing the
88- " search distance and number of syntax lookups that need to take place.
89- function ! s: CheckPair (name, start , end , SkipFn)
90- let prevln = s: best_match [1 ][0 ]
91- let pos = searchpairpos (a: start , ' ' , a: end , ' bznW' , a: SkipFn , prevln)
92- if prevln < pos[0 ] || (prevln == pos[0 ] && s: best_match [1 ][1 ] < pos[1 ])
93- let s: best_match = [a: name , pos]
94- endif
95- endfunction
96-
97- function ! s: GetCurrentSynName (lnum)
98- if empty (getline (a: lnum ))
99- " Improves the accuracy of string detection when a newline is
100- " entered while in insert mode.
101- let strline = a: lnum - 1
102- return s: GetSynIdName (strline, strlen (getline (strline)))
103- else
104- return s: GetSynIdName (a: lnum , 1 )
105- endif
106- endfunction
107-
108149function ! s: GetClojureIndent ()
109- " Move cursor to the first column of the line we want to indent.
110- call cursor (v: lnum , 1 )
111-
112- let s: best_match = [' top' , [0 , 0 ]]
113-
114- let synname = s: GetCurrentSynName (v: lnum )
115- if synname = ~? ' string'
116- call s: CheckPair (' str' , ' "' , ' "' , function (' <SID>NotStringDelimiter' ))
117- " Sometimes, string highlighting does not kick in correctly,
118- " until after this first "s:CheckPair" call, so we have to
119- " detect and attempt an automatic correction.
120- let new_synname = s: GetCurrentSynName (v: lnum )
121- if new_synname !=# synname
122- echoerr ' Misdetected string! Retrying...'
123- let s: best_match = [' top' , [0 , 0 ]]
124- let synname = new_synname
125- endif
126- endif
127-
128- if synname = ~? ' string'
129- " We already checked this above, so pass through this block.
130- elseif synname = ~? ' regex'
131- call s: CheckPair (' rex' , ' #\zs"' , ' "' , function (' <SID>NotRegexpDelimiter' ))
132- else
133- let IgnoredRegionFn = function (' <SID>IgnoredRegion' )
134- if bufname () = ~? ' \.edn$'
135- " If EDN file, check list pair last.
136- call s: CheckPair (' map' , ' {' , ' }' , IgnoredRegionFn)
137- call s: CheckPair (' vec' , ' \[' , ' \]' , IgnoredRegionFn)
138- call s: CheckPair (' lst' , ' (' , ' )' , IgnoredRegionFn)
139- else
140- " If CLJ file, check list pair first.
141- call s: CheckPair (' lst' , ' (' , ' )' , IgnoredRegionFn)
142- call s: CheckPair (' map' , ' {' , ' }' , IgnoredRegionFn)
143- call s: CheckPair (' vec' , ' \[' , ' \]' , IgnoredRegionFn)
144- endif
145- endif
146-
147150 " Calculate and return indent to use based on the matching form.
148- let [formtype, coord] = s: best_match
149- if formtype == # ' top ' | return 0 " At top level, no indent.
150- elseif formtype == # ' lst ' | return s: GetListIndent (coord)
151- elseif formtype == # ' vec ' | return coord[1 ] " Vector
152- elseif formtype == # ' map ' | return coord[1 ] " Map/set
153- elseif formtype == # ' str ' | return s: GetStringIndent (coord, 0 )
154- elseif formtype == # ' rex ' | return s: GetStringIndent (coord, 1 )
155- else | return -1 " Keep existing indent.
151+ let [formtype, coord] = s: InverseRead ( v: lnum )
152+ if formtype == # ' ^ ' | return 0 " At top- level, no indent.
153+ elseif formtype == # ' ( ' | return s: GetListIndent (coord)
154+ elseif formtype == # ' [ ' | return coord[1 ] " Vector
155+ elseif formtype == # ' { ' | return coord[1 ] " Map/set
156+ elseif formtype == # ' " ' | return s: GetStringIndent (coord, 0 )
157+ elseif formtype == # ' #" ' | return s: GetStringIndent (coord, 1 )
158+ else | return -1 " Keep existing indent.
156159 endif
157160endfunction
158161
159- if exists (" *searchpairpos" )
160- setlocal indentexpr = s: GetClojureIndent ()
161- else
162- " If "searchpairpos" is not available, fallback to Lisp indenting.
163- setlocal lisp
164- endif
162+ " TODO: lispoptions if exists.
163+ setlocal indentexpr = s: GetClojureIndent ()
165164
166165let &cpoptions = s: save_cpo
167166unlet ! s: save_cpo
0 commit comments