aboutsummaryrefslogtreecommitdiff
path: root/vim/indent/coffee.vim
blob: a798cfc1751fc1142837a623e732cc32b49123ef (plain)
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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
" Language:    CoffeeScript
" Maintainer:  Mick Koch <kchmck@gmail.com>
" URL:         http://github.com/kchmck/vim-coffee-script
" License:     WTFPL

if exists("b:did_indent")
  finish
endif

let b:did_indent = 1

setlocal autoindent
setlocal indentexpr=GetCoffeeIndent(v:lnum)
" Make sure GetCoffeeIndent is run when these are typed so they can be
" indented or outdented.
setlocal indentkeys+=0],0),0.,=else,=when,=catch,=finally

" Only define the function once.
if exists("*GetCoffeeIndent")
  finish
endif

" Keywords to indent after
let s:INDENT_AFTER_KEYWORD = '^\%(if\|unless\|else\|for\|while\|until\|'
\                          . 'loop\|switch\|when\|try\|catch\|finally\|'
\                          . 'class\)\>'

" Operators to indent after
let s:INDENT_AFTER_OPERATOR = '\%([([{:=]\|[-=]>\)$'

" Keywords and operators that continue a line
let s:CONTINUATION = '\<\%(is\|isnt\|and\|or\)\>$'
\                  . '\|'
\                  . '\%(-\@<!-\|+\@<!+\|<\|[-=]\@<!>\|\*\|/\@<!/\|%\||\|'
\                  . '&\|,\|\.\@<!\.\)$'

" Operators that block continuation indenting
let s:CONTINUATION_BLOCK = '[([{:=]$'

" A continuation dot access
let s:DOT_ACCESS = '^\.'

" Keywords to outdent after
let s:OUTDENT_AFTER = '^\%(return\|break\|continue\|throw\)\>'

" A compound assignment like `... = if ...`
let s:COMPOUND_ASSIGNMENT = '[:=]\s*\%(if\|unless\|for\|while\|until\|'
\                         . 'switch\|try\|class\)\>'

" A postfix condition like `return ... if ...`.
let s:POSTFIX_CONDITION = '\S\s\+\zs\<\%(if\|unless\)\>'

" A single-line else statement like `else ...` but not `else if ...
let s:SINGLE_LINE_ELSE = '^else\s\+\%(\<\%(if\|unless\)\>\)\@!'

" Max lines to look back for a match
let s:MAX_LOOKBACK = 50

" Syntax names for strings
let s:SYNTAX_STRING = 'coffee\%(String\|AssignString\|Embed\|Regex\|Heregex\|'
\                   . 'Heredoc\)'

" Syntax names for comments
let s:SYNTAX_COMMENT = 'coffee\%(Comment\|BlockComment\|HeregexComment\)'

" Syntax names for strings and comments
let s:SYNTAX_STRING_COMMENT = s:SYNTAX_STRING . '\|' . s:SYNTAX_COMMENT

" Get the linked syntax name of a character.
function! s:SyntaxName(linenum, col)
  return synIDattr(synID(a:linenum, a:col, 1), 'name')
endfunction

" Check if a character is in a comment.
function! s:IsComment(linenum, col)
  return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_COMMENT
endfunction

" Check if a character is in a string.
function! s:IsString(linenum, col)
  return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING
endfunction

" Check if a character is in a comment or string.
function! s:IsCommentOrString(linenum, col)
  return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING_COMMENT
endfunction

" Check if a whole line is a comment.
function! s:IsCommentLine(linenum)
  " Check the first non-whitespace character.
  return s:IsComment(a:linenum, indent(a:linenum) + 1)
endfunction

" Repeatedly search a line for a regex until one is found outside a string or
" comment.
function! s:SmartSearch(linenum, regex)
  " Start at the first column.
  let col = 0

  " Search until there are no more matches, unless a good match is found.
  while 1
    call cursor(a:linenum, col + 1)
    let [_, col] = searchpos(a:regex, 'cn', a:linenum)

    " No more matches.
    if !col
      break
    endif

    if !s:IsCommentOrString(a:linenum, col)
      return 1
    endif
  endwhile

  " No good match found.
  return 0
endfunction

" Skip a match if it's in a comment or string, is a single-line statement that
" isn't adjacent, or is a postfix condition.
function! s:ShouldSkip(startlinenum, linenum, col)
  if s:IsCommentOrString(a:linenum, a:col)
    return 1
  endif

  " Check for a single-line statement that isn't adjacent.
  if s:SmartSearch(a:linenum, '\<then\>') && a:startlinenum - a:linenum > 1
    return 1
  endif

  if s:SmartSearch(a:linenum, s:POSTFIX_CONDITION) &&
  \ !s:SmartSearch(a:linenum, s:COMPOUND_ASSIGNMENT)
    return 1
  endif

  return 0
endfunction

" Find the farthest line to look back to, capped to line 1 (zero and negative
" numbers cause bad things).
function! s:MaxLookback(startlinenum)
  return max([1, a:startlinenum - s:MAX_LOOKBACK])
endfunction

" Get the skip expression for searchpair().
function! s:SkipExpr(startlinenum)
  return "s:ShouldSkip(" . a:startlinenum . ", line('.'), col('.'))"
endfunction

" Search for pairs of text.
function! s:SearchPair(start, end)
  " The cursor must be in the first column for regexes to match.
  call cursor(0, 1)

  let startlinenum = line('.')

  " Don't need the W flag since MaxLookback caps the search to line 1.
  return searchpair(a:start, '', a:end, 'bcn',
  \                 s:SkipExpr(startlinenum),
  \                 s:MaxLookback(startlinenum))
endfunction

" Try to find a previous matching line.
function! s:GetMatch(curline)
  let firstchar = a:curline[0]

  if firstchar == '}'
    return s:SearchPair('{', '}')
  elseif firstchar == ')'
    return s:SearchPair('(', ')')
  elseif firstchar == ']'
    return s:SearchPair('\[', '\]')
  elseif a:curline =~ '^else\>'
    return s:SearchPair('\<\%(if\|unless\|when\)\>', '\<else\>')
  elseif a:curline =~ '^catch\>'
    return s:SearchPair('\<try\>', '\<catch\>')
  elseif a:curline =~ '^finally\>'
    return s:SearchPair('\<try\>', '\<finally\>')
  endif

  return 0
endfunction

" Get the nearest previous line that isn't a comment.
function! s:GetPrevNormalLine(startlinenum)
  let curlinenum = a:startlinenum

  while curlinenum > 0
    let curlinenum = prevnonblank(curlinenum - 1)

    if !s:IsCommentLine(curlinenum)
      return curlinenum
    endif
  endwhile

  return 0
endfunction

" Try to find a comment in a line.
function! s:FindComment(linenum)
  let col = 0

  while 1
    call cursor(a:linenum, col + 1)
    let [_, col] = searchpos('#', 'cn', a:linenum)

    if !col
      break
    endif

    if s:IsComment(a:linenum, col)
      return col
    endif
  endwhile

  return 0
endfunction

" Get a line without comments or surrounding whitespace.
function! s:GetTrimmedLine(linenum)
  let comment = s:FindComment(a:linenum)
  let line = getline(a:linenum)

  if comment
    " Subtract 1 to get to the column before the comment and another 1 for
    " zero-based indexing.
    let line = line[:comment - 2]
  endif

  return substitute(substitute(line, '^\s\+', '', ''),
  \                                  '\s\+$', '', '')
endfunction

function! s:GetCoffeeIndent(curlinenum)
  let prevlinenum = s:GetPrevNormalLine(a:curlinenum)

  " Don't do anything if there's no previous line.
  if !prevlinenum
    return -1
  endif

  let curline = s:GetTrimmedLine(a:curlinenum)

  " Try to find a previous matching statement. This handles outdenting.
  let matchlinenum = s:GetMatch(curline)

  if matchlinenum
    return indent(matchlinenum)
  endif

  " Try to find a matching `when`.
  if curline =~ '^when\>' && !s:SmartSearch(prevlinenum, '\<switch\>')
    let linenum = a:curlinenum

    while linenum > 0
      let linenum = s:GetPrevNormalLine(linenum)

      if getline(linenum) =~ '^\s*when\>'
        return indent(linenum)
      endif
    endwhile

    return -1
  endif

  let prevline = s:GetTrimmedLine(prevlinenum)
  let previndent = indent(prevlinenum)

  " Always indent after these operators.
  if prevline =~ s:INDENT_AFTER_OPERATOR
    return previndent + &shiftwidth
  endif

  " Indent after a continuation if it's the first.
  if prevline =~ s:CONTINUATION
    let prevprevlinenum = s:GetPrevNormalLine(prevlinenum)

    " If the continuation is the first in the file, don't run the other checks.
    if !prevprevlinenum
      return previndent + &shiftwidth
    endif

    let prevprevline = s:GetTrimmedLine(prevprevlinenum)

    if prevprevline !~ s:CONTINUATION && prevprevline !~ s:CONTINUATION_BLOCK
      return previndent + &shiftwidth
    endif

    return -1
  endif

  " Indent after these keywords and compound assignments if they aren't a
  " single-line statement.
  if prevline =~ s:INDENT_AFTER_KEYWORD || prevline =~ s:COMPOUND_ASSIGNMENT
    if !s:SmartSearch(prevlinenum, '\<then\>') && prevline !~ s:SINGLE_LINE_ELSE
      return previndent + &shiftwidth
    endif

    return -1
  endif

  " Indent a dot access if it's the first.
  if curline =~ s:DOT_ACCESS && prevline !~ s:DOT_ACCESS
    return previndent + &shiftwidth
  endif

  " Outdent after these keywords if they don't have a postfix condition or are
  " a single-line statement.
  if prevline =~ s:OUTDENT_AFTER
    if !s:SmartSearch(prevlinenum, s:POSTFIX_CONDITION) ||
    \   s:SmartSearch(prevlinenum, '\<then\>')
      return previndent - &shiftwidth
    endif
  endif

  " No indenting or outdenting is needed.
  return -1
endfunction

" Wrap s:GetCoffeeIndent to keep the cursor position.
function! GetCoffeeIndent(curlinenum)
  let oldcursor = getpos('.')
  let indent = s:GetCoffeeIndent(a:curlinenum)
  call setpos('.', oldcursor)

  return indent
endfunction