@@ -21,32 +21,36 @@ setlocal noautoindent nosmartindent nolisp
21
21
setlocal softtabstop = 2 shiftwidth = 2 expandtab
22
22
setlocal indentkeys = ! ,o ,O
23
23
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]
27
30
return (strlen (ln ) - strlen(trim(ln, '\', 2))) % 2
28
31
endfunction
29
32
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)
36
37
let tokens = []
37
38
let ln = getline(a:line_num)
38
39
39
40
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.
43
43
let token = searchpos (' [()\[\]{};"]' , ' bW' , a: line_num )
44
44
45
+ " No more matches, exit loop.
45
46
if token == [0 , 0 ] | break | endif
47
+
46
48
let t_idx = token[1 ] - 1
49
+
50
+ " Escaped character, ignore.
47
51
if s: IsEscaped (ln , t_idx) | continue | endif
48
- let t_char = ln [t_idx]
49
52
53
+ let t_char = ln [t_idx]
50
54
if t_char == # ' ;'
51
55
" Comment found, reset the token list for this line.
52
56
tokens = []
@@ -59,61 +63,79 @@ function! s:AnalyseLine(line_num)
59
63
return tokens
60
64
endfunction
61
65
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]], ...]".
66
75
let tokens = []
76
+ let first_string_pos = []
77
+ let in_string = 0
67
78
79
+ let lnum = a: lnum - 1
68
80
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
71
88
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
73
95
74
- " Reduce "tokens" and "line_tokens".
75
- for t in line_tokens
76
- " TODO: attempt early termination.
96
+ " TODO: early termination?
77
97
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 ] == # ' "'
82
100
call remove (tokens, -1 )
83
- elseif get (s: pairs , t [0 ], ' ' ) == # tokens[-1 ][0 ]
101
+ elseif get (s: pairs , tk [0 ], ' ' ) == # tokens[-1 ][0 ]
84
102
" Matching pair: drop the last item in tokens.
85
103
call remove (tokens, -1 )
86
104
else
87
105
" No match: append to token list.
88
- call add (tokens, t )
106
+ call add (tokens, tk )
89
107
endif
90
108
endfor
91
109
92
110
" echom 'Pass' lnum tokens
93
111
94
112
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
113
return tokens[0 ]
98
114
endif
99
115
100
116
let lnum -= 1
101
117
endwhile
102
118
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
+
103
125
return [' ^' , [0 , 0 ]] " Default to top-level.
104
126
endfunction
105
127
128
+ " Get the value of a configuration option.
106
129
function ! s: Conf (opt , default)
107
130
return get (b: , a: opt , get (g: , a: opt , a: default ))
108
131
endfunction
109
132
133
+ " Returns "1" when the previous operator used was "=" and is currently active.
110
134
function ! s: EqualsOperatorInEffect ()
111
- " Returns 1 when the previous operator used is "=" and is currently in
112
- " effect (i.e. "state" includes "o").
113
135
return v: operator == # ' =' && state (' o' ) == # ' o'
114
136
endfunction
115
137
116
- function ! s: GetStringIndent (delim_pos, is_regex )
138
+ function ! s: StringIndent (delim_pos)
117
139
" Mimic multi-line string indentation behaviour in VS Code and Emacs.
118
140
let m = mode ()
119
141
if m == # ' i' || (m == # ' n' && ! s: EqualsOperatorInEffect ())
@@ -124,43 +146,45 @@ function! s:GetStringIndent(delim_pos, is_regex)
124
146
" 1: Indent in alignment with string start delimiter.
125
147
if alignment == -1 | return 0
126
148
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 )
128
153
endif
129
154
else
130
155
return -1 " Keep existing indent.
131
156
endif
132
157
endfunction
133
158
134
- function ! s: GetListIndent (delim_pos)
135
- " TODO Begin analysis and apply rules!
159
+ function ! s: ListIndent (delim_pos)
136
160
" 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 )
139
164
if sym != -1 && ! empty (sym) && match (sym, ' ^[0-9:]' ) == -1
140
165
" TODO: align indentation.
141
166
" TODO: lookup rules.
142
- return delim_pos[ 1 ] + 1 " 2 space indentation
167
+ return delim_col + 1 " 2 space indentation
143
168
endif
144
169
145
170
" TODO: switch between 1 vs 2 space indentation.
146
- return delim_pos[ 1 ] " 1 space indentation
171
+ return delim_col " 1 space indentation
147
172
endfunction
148
173
149
- function ! s: GetClojureIndent ()
174
+ function ! s: ClojureIndent ()
150
175
" 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.
159
183
endif
160
184
endfunction
161
185
162
- " TODO: lispoptions if exists.
163
- setlocal indentexpr = s: GetClojureIndent ()
186
+ " TODO: set lispoptions if exists.
187
+ setlocal indentexpr = s: ClojureIndent ()
164
188
165
189
let &cpoptions = s: save_cpo
166
190
unlet ! s: save_cpo
0 commit comments