Skip to content

Commit ee2acc2

Browse files
committed
Completed the mini Clojure reader as the core of the indentation system
Some refactoring should be possible here and further optimisations. Once all optimisations I can think of have been implemented, I'll try writing an alternate Vim9 script version. (The syntax highlight group checks used in previous implementations of the indentation code was the core bottleneck, so a Vim9 script version would not have been much faster.)
1 parent cc9cda7 commit ee2acc2

File tree

1 file changed

+76
-52
lines changed

1 file changed

+76
-52
lines changed

indent/clojure.vim

+76-52
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,36 @@ setlocal noautoindent nosmartindent nolisp
2121
setlocal softtabstop=2 shiftwidth=2 expandtab
2222
setlocal 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
2831
endfunction
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
6064
endfunction
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.
104126
endfunction
105127

128+
" Get the value of a configuration option.
106129
function! s:Conf(opt, default)
107130
return get(b:, a:opt, get(g:, a:opt, a:default))
108131
endfunction
109132

133+
" Returns "1" when the previous operator used was "=" and is currently active.
110134
function! 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'
114136
endfunction
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
132157
endfunction
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
147172
endfunction
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
160184
endfunction
161185

162-
" TODO: lispoptions if exists.
163-
setlocal indentexpr=s:GetClojureIndent()
186+
" TODO: set lispoptions if exists.
187+
setlocal indentexpr=s:ClojureIndent()
164188

165189
let &cpoptions = s:save_cpo
166190
unlet! s:save_cpo

0 commit comments

Comments
 (0)