@@ -21,24 +21,86 @@ setlocal noautoindent nosmartindent nolisp
21
21
setlocal softtabstop = 2 shiftwidth = 2 expandtab
22
22
setlocal indentkeys = ! ,o ,O
23
23
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
26
28
endfunction
27
29
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
31
58
32
- function ! s: IgnoredRegion ()
33
- return s: SyntaxMatch (' \%(string\|regex\|comment\|character\)' , line (' .' ), col (' .' ))
59
+ return tokens
34
60
endfunction
35
61
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
39
102
40
- function ! s: NotRegexpDelimiter ()
41
- return ! s: SyntaxMatch (' regexpdelimiter' , line (' .' ), col (' .' ))
103
+ return [' ^' , [0 , 0 ]] " Default to top-level.
42
104
endfunction
43
105
44
106
function ! s: Conf (opt , default)
@@ -51,7 +113,7 @@ function! s:EqualsOperatorInEffect()
51
113
return v: operator == # ' =' && state (' o' ) == # ' o'
52
114
endfunction
53
115
54
- function ! s: GetStringIndent (delim_pos, regex )
116
+ function ! s: GetStringIndent (delim_pos, is_regex )
55
117
" Mimic multi-line string indentation behaviour in VS Code and Emacs.
56
118
let m = mode ()
57
119
if m == # ' i' || (m == # ' n' && ! s: EqualsOperatorInEffect ())
@@ -62,7 +124,7 @@ function! s:GetStringIndent(delim_pos, regex)
62
124
" 1: Indent in alignment with string start delimiter.
63
125
if alignment == -1 | return 0
64
126
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 )
66
128
endif
67
129
else
68
130
return -1 " Keep existing indent.
@@ -71,97 +133,34 @@ endfunction
71
133
72
134
function ! s: GetListIndent (delim_pos)
73
135
" TODO Begin analysis and apply rules!
136
+ " let lns = getline(delim_pos[0], v:lnum - 1)
74
137
let ln1 = getline (delim_pos[0 ])
75
138
let sym = get (split (ln1[delim_pos[1 ]:], ' [[:space:],;()\[\]{}@\\"^~`]' , 1 ), 0 , -1 )
76
139
if sym != -1 && ! empty (sym) && match (sym, ' ^[0-9:]' ) == -1
77
140
" TODO: align indentation.
141
+ " TODO: lookup rules.
78
142
return delim_pos[1 ] + 1 " 2 space indentation
79
143
endif
80
144
81
145
" TODO: switch between 1 vs 2 space indentation.
82
146
return delim_pos[1 ] " 1 space indentation
83
147
endfunction
84
148
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
-
108
149
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
-
147
150
" 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.
156
159
endif
157
160
endfunction
158
161
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 ()
165
164
166
165
let &cpoptions = s: save_cpo
167
166
unlet ! s: save_cpo
0 commit comments