1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
" Vim indent file
" Language: Haskell
" Maintainer: Tristan Ravitch
if exists('b:did_indent')
finish
endif
let b:did_indent = 1
if !exists('g:hasksyn_indent_search_backward')
let g:hasksyn_indent_search_backward = 100
endif
if !exists('g:hasksyn_dedent_after_return')
let g:hasksyn_dedent_after_return = 1
endif
if !exists('g:hasksyn_dedent_after_catchall_case')
let g:hasksyn_dedent_after_catchall_case = 1
endif
setlocal noautoindent
setlocal indentexpr=HIndent(v:lnum)
setlocal indentkeys+=0=where
setlocal indentkeys+=0=->
setlocal indentkeys+=0==>
setlocal indentkeys+=0=in
setlocal indentkeys+=0=class,0=instance,0=import
setlocal indentkeys+=<Bar>
setlocal indentkeys+=0\,
if exists("*HIndent")
finish
endif
function! HIndent(lnum)
" Don't do anything boneheaded if we are inside of a block comment
if s:IsInBlockComment()
return -1
endif
let plnum = s:PrevNonCommentLineNum(a:lnum)
if plnum == 0
return 0
endif
let prevl = s:GetAndStripTrailingComments(plnum)
let thisl = s:GetAndStripTrailingComments(a:lnum)
let previ = indent(plnum)
" If this is a bare where clause, indent it one step. where as part of an
" instance should be unaffected unless you put it in an odd place.
" This is the wrong thing if you are deeply indented already and want to put
" a where clause on the top-level construct, but there isn't much that can
" be done about that case...
if thisl =~ '^\s*where\s*$'
return previ + &sw
endif
" If we start a new line for a type signature, see if we can line it up with
" the previous line.
if thisl =~ '^\s*\(->\|=>\)\s*'
let tokPos = s:BackwardPatternSearch(a:lnum, '\(::\|->\|=>\)')
if tokPos != -1
return tokPos
endif
endif
if prevl =~ '\Wof\s*$' || prevl =~ '\Wm\=do\s*$'
return previ + &sw
endif
" Now for commas. Commas will align pretty naturally for simple pattern
" guards, so don't worry about that for now. If we see the line is just a
" comma, search up for something to align it to. In the easy case, look
" for a [ or { (the last in their line). Also consider other commas that
" are preceeded only by whitespace. This isn't just a previous line check
" necessarily, though that would cover most cases.
if thisl =~ '^\s*,'
let cmatch = match(prevl, '\(^\s*\)\@<=,')
if cmatch != -1
return cmatch
endif
let bmatch = match(prevl, '\({\|\[\)')
if bmatch != -1
return bmatch
endif
endif
" Match an 'in' keyword with the corresponding let. Unfortunately, if the
" name of your next binding happens to start with 'in', this will muck with
" it. Not sure if there is a workaround because we can't force an
" auto-indent after 'in ' as far as I can see.
if thisl =~ '\s*in$'
let letStart = s:BackwardPatternSearch(a:lnum, '\(\W\)\@<=let\W')
if letStart != -1
return letStart
endif
endif
" We don't send data or type to column zero because they can be indented
" inside of 'class' definitions for data/type families
if thisl =~ '^\s*\(class\|instance\|newtype\|import\)'
return 0
endif
" FIXME: Only do this if the previous line was not already indented for the
" same reason. Also be careful of -> in type signatures. Make sure we have
" an earlier rule to line those up properly.
if prevl =~ '[=>\$\.\^+\&`(-]\s*$'
return previ + &sw
endif
" We have a special case for dealing with trailing '*' operators. If the *
" is the end of a kind signature in a type family/associated type, we don't
" want to indent the next line. We do if it is just being a * operator in
" an expression, though.
if prevl =~ '\(\(type\|data\).*\)\@<!\*\s*$'
return previ + &sw
endif
" If the previous line ends in a where, indent us a step
if prevl =~ '\Wwhere\s*$'
return previ + &sw
endif
" If we see a |, first try to line it up with the pipe on the previous line.
" Search backward on nearby lines, giving up if we hit a line with a \w at
" column 0. Otherwise, indent it relative to the previous line
"
" Here we can also handle the case of lining up data declarations. The
" backwards pipe search will fail for a data declaration (since data is at
" column 0), so we can have an extra check after the pipe search for
" data..=.
if thisl =~ '^\s*|$'
let nearestPipeIndex = s:BackwardPatternSearch(a:lnum, '\(^\s*\)\@<=|')
if nearestPipeIndex != -1
return nearestPipeIndex
endif
let dataEquals = match(prevl, '\(data.*\)\@<==')
if dataEquals != -1
return dataEquals
endif
return previ + &sw
endif
" If the previous line has a let, line the cursor up with the start of the
" first binding name. Autoindent handles subsequent cases.
"
" This should come after the 'in' aligner so that 'in' is not treated as
" just something to be aligned to the previous binding.
let lbindStart = match(prevl, '\(\Wlet\s\+\)\@<=\w')
if lbindStart != -1
return lbindStart
endif
" If requested, dedent from a bare return (presumably in a do block).
" This comes after the trailing operator case - hopefully that will avoid
" returns on lines by themselves but not really in a do block. This is a
" heuristic.
if g:hasksyn_dedent_after_return && prevl =~ '^\s*return\W'
return previ - &sw
endif
" Similar to the return dedent - after a catchall case _ -> ..., we can
" almost certainly dedent. Again, it comes after the line continuation
" heuristic so we don't dedent while someone is making an obviously
" multi-line construct
if g:hasksyn_dedent_after_catchall_case && prevl =~ '^\s*_\s*->\W'
return previ - &sw
endif
" On the other hand, if the previous line is a where with some bindings
" following it on the same line, accommodate and align with the first non-ws
" char after the where
if prevl =~ '\Wwhere\s\+\w'
let bindStart = match(prevl, '\(\Wwhere\s\+\)\@<=\w')
if bindStart != -1
return bindStart
endif
return previ + &sw
endif
return previ
endfunction
" Search backwards for a token from the cursor position
function! s:FindTokenNotInCommentOrString(tok)
return search('\(--.*\|"\([^"]\|\\"\)*\)\@<!' . tok, 'bcnW')
endfunction
" Should return -1 if the given line is inside of an unclosed block comment.
" This is meant to let us exit early from the indenter if we are in a comment.
" Look for the nearest -} and {- such that they are not between "" or in a
" line comment
"
" Note: we may need to restrict how far back this will search. On the other
" hand, the native vim 'search' function might be efficient enough to support
" entire buffers.
function! s:IsInBlockComment()
let openCommPos = s:FindTokenNotInCommentOrString('{-')
" If there is no open comment, then we don't have to look for a close
if openCommPos == 0
return 0
endif
" Or if there is a close comment marker that comes after the open marker, we
" are not in a comment. Note that we potentially need to check the position
" in the line if they are both on the same line. I'll fix it later.
let closeCommPos = s:FindTokenNotInCommentOrString('-}')
if closeCommPos >= openCommPos
return 0
endif
return 1
endfunction
" Get the previous line that is not a comment. Pass in the *current* line
" number. Also skips blank lines.
function! s:PrevNonCommentLineNum(lnum)
if a:lnum <= 1
return 0
endif
let lnum = a:lnum - 1
while 1
if lnum == 0
return 0
endif
let aline = getline(lnum)
if aline =~ '^\s*--'
let lnum = lnum - 1
else
return lnum
endif
endwhile
endfunction
function! s:GetAndStripTrailingComments(lnum)
let aline = getline(a:lnum)
" We can't just remove the string literal since that leaves us with a
" trailing operator (=), so replace it with a fake identifier
let noStrings = substitute(aline, '"\([^"]\|\\"\)*"', 's', '')
let noLineCom = substitute(noStrings, '--.*$', '', '')
" If there are no fancy block comments involved, skip some of this extra
" work
if noLineCom !~ '\({-\|-}\)'
return noLineCom
endif
" We stripped line comments, now we need to strip out any relevant multiline
" comments. This includes comments starting much earlier but ending on this
" line or comments starting on this line and continuing to the next. This
" is probably easiest in two steps: {- to (-}|$) and then ^ to -}.
" Note we are using a non-greedy match here so that only the minimal {- -}
" pair is consumed.
let noBlock1 = substitute(noLineComm, '{-.\{-}-}', '', '')
let noBlock2 = substitute(noBlock1, '{-.\{-}$', '', '')
let noBlock3 = substitute(noBlock2, '^.\{-}-}', '', '')
return noBlock3
endfunction
" Search backwards from lnum for pat, returning the starting index if found
" within the search range or -1 if not found. Stops searching at lines
" starting at column 0 with an identifier character.
function! s:BackwardPatternSearch(lnum, pat)
let lnum = s:PrevNonCommentLineNum(a:lnum)
while 1
let aline = s:GetAndStripTrailingComments(lnum)
if a:lnum - lnum > g:hasksyn_indent_search_backward
return -1
endif
let theMatch = match(aline, a:pat)
if theMatch != -1
return theMatch
else
" We want to be able to consider lines starting in column 0, but we don't
" want to search back past them.
if aline =~ '^\w'
return -1
endif
let lnum = s:PrevNonCommentLineNum(lnum)
endif
endwhile
endfunction
|