@@ -21,32 +21,36 @@ setlocal noautoindent nosmartindent nolisp
2121setlocal softtabstop = 2 shiftwidth = 2 expandtab
2222setlocal indentkeys = ! ,o ,O
2323
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]
24+ " TODO: After all optimisations create Vim9script variant of the core algorithm.
25+
26+ " Returns "1" if position "i_char" in "line_str" is preceded by an odd number
27+ " of backslash characters (i.e. escaped).
28+ function ! s: IsEscaped (line_str, i_char)
29+ let ln = a:line_str[: a:i_char - 1]
2730 return (strlen (ln ) - strlen(trim(ln, '\', 2))) % 2
2831endfunction
2932
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)
33+ " Repeatedly search for tokens on a given line (in reverse order) building up
34+ " a list of tokens and their positions. Ignores escaped tokens. Does not
35+ " care about strings, as that is handled by "s:InsideForm".
36+ function ! s: TokeniseLine (line_num)
3637 let tokens = []
3738 let ln = getline(a:line_num)
3839
3940 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.
41+ " We perform searches within the buffer (and move the cusor)
42+ " for better performance than looping char by char in a line.
4343 let token = searchpos (' [()\[\]{};"]' , ' bW' , a: line_num )
4444
45+ " No more matches, exit loop.
4546 if token == [0 , 0 ] | break | endif
47+
4648 let t_idx = token[1 ] - 1
49+
50+ " Escaped character, ignore.
4751 if s: IsEscaped (ln , t_idx) | continue | endif
48- let t_char = ln [t_idx]
4952
53+ let t_char = ln [t_idx]
5054 if t_char == # ' ;'
5155 " Comment found, reset the token list for this line.
5256 tokens = []
@@ -59,61 +63,79 @@ function! s:AnalyseLine(line_num)
5963 return tokens
6064endfunction
6165
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 s: pairs = {' (' : ' )' , ' [' : ' ]' , ' {' : ' }' }
67+
68+ " TODO: refactor this procedure and optimise.
69+ " This procedure is essentially a lightweight Clojure reader.
70+ function ! s: InsideForm (lnum)
71+ " Reset cursor to first column of the line we wish to indent.
72+ call cursor (a: lnum , 1 )
73+
74+ " Token list looks like this: "[[delim, [line, col]], ...]".
6675 let tokens = []
76+ let first_string_pos = []
77+ let in_string = 0
6778
79+ let lnum = a: lnum - 1
6880 while lnum > 0
69- call cursor (lnum + 1 , 1 )
70- let line_tokens = s: AnalyseLine (lnum)
81+ " Reduce tokens from line "lnum" into "tokens".
82+ for tk in s: TokeniseLine (lnum)
83+ " Keep track of the first string delimiter we see, as
84+ " we'll need it later for multi-line strings/regexps.
85+ if first_string_pos == [] && tk[0 ] == # ' "'
86+ let first_string_pos = tk[1 ]
87+ endif
7188
72- " let should_ignore = empty(a:tokens) ? 0 : (a:tokens[-1][0] ==# '"')
89+ " When in string ignore other tokens.
90+ if in_string && tk[0 ] !=# ' "'
91+ continue
92+ else
93+ let in_string = 0
94+ endif
7395
74- " Reduce "tokens" and "line_tokens".
75- for t in line_tokens
76- " TODO: attempt early termination.
96+ " TODO: early termination?
7797 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.
98+ call add (tokens, tk)
99+ elseif tk[0 ] == # ' "' && tokens[-1 ][0 ] == # ' "'
82100 call remove (tokens, -1 )
83- elseif get (s: pairs , t [0 ], ' ' ) == # tokens[-1 ][0 ]
101+ elseif get (s: pairs , tk [0 ], ' ' ) == # tokens[-1 ][0 ]
84102 " Matching pair: drop the last item in tokens.
85103 call remove (tokens, -1 )
86104 else
87105 " No match: append to token list.
88- call add (tokens, t )
106+ call add (tokens, tk )
89107 endif
90108 endfor
91109
92110 " echom 'Pass' lnum tokens
93111
94112 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]
97113 return tokens[0 ]
98114 endif
99115
100116 let lnum -= 1
101117 endwhile
102118
119+ if ! empty (tokens) && tokens[0 ][0 ] == # ' "'
120+ " Must have been in a multi-line string or regular expression
121+ " as the string was never closed.
122+ return [' "' , first_string_pos]
123+ endif
124+
103125 return [' ^' , [0 , 0 ]] " Default to top-level.
104126endfunction
105127
128+ " Get the value of a configuration option.
106129function ! s: Conf (opt , default)
107130 return get (b: , a: opt , get (g: , a: opt , a: default ))
108131endfunction
109132
133+ " Returns "1" when the previous operator used was "=" and is currently active.
110134function ! s: EqualsOperatorInEffect ()
111- " Returns 1 when the previous operator used is "=" and is currently in
112- " effect (i.e. "state" includes "o").
113135 return v: operator == # ' =' && state (' o' ) == # ' o'
114136endfunction
115137
116- function ! s: GetStringIndent (delim_pos, is_regex )
138+ function ! s: StringIndent (delim_pos)
117139 " Mimic multi-line string indentation behaviour in VS Code and Emacs.
118140 let m = mode ()
119141 if m == # ' i' || (m == # ' n' && ! s: EqualsOperatorInEffect ())
@@ -124,43 +146,45 @@ function! s:GetStringIndent(delim_pos, is_regex)
124146 " 1: Indent in alignment with string start delimiter.
125147 if alignment == -1 | return 0
126148 elseif alignment == 1 | return a: delim_pos [1 ]
127- else | return a: delim_pos [1 ] - (a: is_regex ? 2 : 1 )
149+ else
150+ let col = a: delim_pos [1 ]
151+ let is_regex = col > 1 && getline (a: delim_pos [0 ])[col - 2 ] == # ' #'
152+ return col - (is_regex ? 2 : 1 )
128153 endif
129154 else
130155 return -1 " Keep existing indent.
131156 endif
132157endfunction
133158
134- function ! s: GetListIndent (delim_pos)
135- " TODO Begin analysis and apply rules!
159+ function ! s: ListIndent (delim_pos)
136160 " let lns = getline(delim_pos[0], v:lnum - 1)
137- let ln1 = getline (delim_pos[0 ])
138- let sym = get (split (ln1[delim_pos[1 ]:], ' [[:space:],;()\[\]{}@\\"^~`]' , 1 ), 0 , -1 )
161+ let ln1 = getline (a: delim_pos [0 ])
162+ let delim_col = a: delim_pos [1 ]
163+ let sym = get (split (ln1[delim_col: ], ' [[:space:],;()\[\]{}@\\"^~`]' , 1 ), 0 , -1 )
139164 if sym != -1 && ! empty (sym) && match (sym, ' ^[0-9:]' ) == -1
140165 " TODO: align indentation.
141166 " TODO: lookup rules.
142- return delim_pos[ 1 ] + 1 " 2 space indentation
167+ return delim_col + 1 " 2 space indentation
143168 endif
144169
145170 " TODO: switch between 1 vs 2 space indentation.
146- return delim_pos[ 1 ] " 1 space indentation
171+ return delim_col " 1 space indentation
147172endfunction
148173
149- function ! s: GetClojureIndent ()
174+ function ! s: ClojureIndent ()
150175 " Calculate and return indent to use based on the matching form.
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.
176+ let [form, pos] = s: InsideForm (v: lnum )
177+ if form == # ' ^' | return 0 " At top-level, no indent.
178+ elseif form == # ' (' | return s: ListIndent (pos)
179+ elseif form == # ' [' | return pos[1 ]
180+ elseif form == # ' {' | return pos[1 ]
181+ elseif form == # ' "' | return s: StringIndent (pos)
182+ else | return -1 " Keep existing indent.
159183 endif
160184endfunction
161185
162- " TODO: lispoptions if exists.
163- setlocal indentexpr = s: GetClojureIndent ()
186+ " TODO: set lispoptions if exists.
187+ setlocal indentexpr = s: ClojureIndent ()
164188
165189let &cpoptions = s: save_cpo
166190unlet ! s: save_cpo
0 commit comments