Skip to content

Commit cc9cda7

Browse files
committed
Start of a small Clojure reader for indentation w/o syntax highlighting
Not complete yet, but getting there.
1 parent 51f5dcb commit cc9cda7

File tree

1 file changed

+88
-89
lines changed

1 file changed

+88
-89
lines changed

indent/clojure.vim

+88-89
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,86 @@ setlocal noautoindent nosmartindent nolisp
2121
setlocal softtabstop=2 shiftwidth=2 expandtab
2222
setlocal 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
2628
endfunction
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
3460
endfunction
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.
42104
endfunction
43105

44106
function! s:Conf(opt, default)
@@ -51,7 +113,7 @@ function! s:EqualsOperatorInEffect()
51113
return v:operator ==# '=' && state('o') ==# 'o'
52114
endfunction
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

72134
function! 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
83147
endfunction
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-
108149
function! 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
157160
endfunction
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

166165
let &cpoptions = s:save_cpo
167166
unlet! s:save_cpo

0 commit comments

Comments
 (0)