summaryrefslogtreecommitdiffstats
path: root/indent/crystal.vim
blob: 96cdd58615f66d79107cc658c5f29dfe4dabba80 (plain) (blame)
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
if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'crystal') == -1

" Only load this indent file when no other was loaded.
if exists('b:did_indent')
  finish
endif
let b:did_indent = 1

setlocal nosmartindent

" Now, set up our indentation expression and keys that trigger it.
setlocal indentexpr=GetCrystalIndent(v:lnum)
setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,.
setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue
setlocal indentkeys+==private,=protected

let s:cpo_save = &cpo
set cpo&vim

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

" Return the value of a single shift-width
if exists('*shiftwidth')
  let s:sw = function('shiftwidth')
else
  function s:sw()
    return &shiftwidth
  endfunction
endif

" GetCrystalIndent Function {{{1
" =========================

function GetCrystalIndent(...)
  " Setup {{{2
  " -----

  " For the current line, use the first argument if given, else v:lnum
  let clnum = a:0 ? a:1 : v:lnum

  " Set up variables for restoring position in file
  let vcol = col('.')

  " Work on the current line {{{2
  " ------------------------

  " Get the current line.
  let line = getline(clnum)
  let ind = -1

  " If we got a closing bracket on an empty line, find its match and indent
  " according to it.  For parentheses we indent to its column - 1, for the
  " others we indent to the containing line's MSL's level.  Return -1 if fail.
  let col = matchend(line, '^\s*[]})]')
  if col > 0 && !crystal#indent#IsInStringOrComment(clnum, col)
    call cursor(clnum, col)
    let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
    if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', g:crystal#indent#skip_expr)
      if line[col-1] ==# ')' && col('.') != col('$') - 1
        let ind = virtcol('.') - 1
      else
        let ind = indent(crystal#indent#GetMSL(line('.')))
      endif
    endif
    return ind
  endif

  " If we have a deindenting keyword, find its match and indent to its level.
  " TODO: this is messy
  if crystal#indent#Match(clnum, g:crystal#indent#crystal_deindent_keywords)
    call cursor(clnum, 1)
    if searchpair(
          \ g:crystal#indent#end_start_regex,
          \ g:crystal#indent#end_middle_regex,
          \ g:crystal#indent#end_end_regex,
          \ 'bW', g:crystal#indent#skip_expr)
      let msl  = crystal#indent#GetMSL(line('.'))
      let line = getline(line('.'))

      if strpart(line, 0, col('.') - 1) =~# '=\s*$' &&
            \ strpart(line, col('.') - 1, 2) !~# 'do'
        " assignment to case/begin/etc, on the same line, hanging indent
        let ind = virtcol('.') - 1
      elseif getline(msl) =~# '=\s*\(#.*\)\=$'
        " in the case of assignment to the msl, align to the starting line,
        " not to the msl
        let ind = indent(line('.'))
      else
        " align to the msl
        let ind = indent(msl)
      endif
    endif
    return ind
  endif

  " If we are in a multi-line string, don't do anything to it.
  if crystal#indent#IsInString(clnum, matchend(line, '^\s*') + 1)
    return indent('.')
  endif

  " If we are at the closing delimiter of a "<<" heredoc-style string, set the
  " indent to 0.
  if line =~# '^\k\+\s*$'
        \ && crystal#indent#IsInStringDelimiter(clnum, 1)
        \ && search('\V<<'.line, 'nbW')
    return 0
  endif

  " If the current line starts with a leading operator, add a level of indent.
  if crystal#indent#Match(clnum, g:crystal#indent#leading_operator_regex)
    return indent(crystal#indent#GetMSL(clnum)) + s:sw()
  endif

  " Work on the previous line. {{{2
  " --------------------------

  " Find a non-blank, non-multi-line string line above the current line.
  let lnum = crystal#indent#PrevNonBlankNonString(clnum - 1)

  " If the line is empty and inside a string, use the previous line.
  if line =~# '^\s*$' && lnum != prevnonblank(clnum - 1)
    return indent(prevnonblank(clnum))
  endif

  " At the start of the file use zero indent.
  if lnum == 0
    return 0
  endif

  " Set up variables for the previous line.
  let line = getline(lnum)
  let ind = indent(lnum)

  if crystal#indent#Match(lnum, g:crystal#indent#continuable_regex) &&
        \ crystal#indent#Match(lnum, g:crystal#indent#continuation_regex)
    return indent(crystal#indent#GetMSL(lnum)) + s:sw() * 2
  endif

  " If the previous line ended with a block opening, add a level of indent.
  if crystal#indent#Match(lnum, g:crystal#indent#block_regex)
    let msl = crystal#indent#GetMSL(lnum)

    if getline(msl) =~# '=\s*\(#.*\)\=$'
      " in the case of assignment to the msl, align to the starting line,
      " not to the msl
      let ind = indent(lnum) + s:sw()
    else
      let ind = indent(msl) + s:sw()
    endif
    return ind
  endif

  " If the previous line started with a leading operator, use its MSL's level
  " of indent
  if crystal#indent#Match(lnum, g:crystal#indent#leading_operator_regex)
    return indent(crystal#indent#GetMSL(lnum))
  endif

  " If the previous line ended with the "*" of a splat, add a level of indent
  if line =~ g:crystal#indent#splat_regex
    return indent(lnum) + s:sw()
  endif

  " If the previous line contained unclosed opening brackets and we are still
  " in them, find the rightmost one and add indent depending on the bracket
  " type.
  "
  " If it contained hanging closing brackets, find the rightmost one, find its
  " match and indent according to that.
  if line =~# '[[({]' || line =~# '[])]\s*\%(#.*\)\=$'
    let [opening, closing] = crystal#indent#ExtraBrackets(lnum)

    if opening.pos != -1
      if opening.type ==# '(' && searchpair('(', '', ')', 'bW', g:crystal#indent#skip_expr)
        if col('.') + 1 == col('$')
          return ind + s:sw()
        else
          return virtcol('.')
        endif
      else
        let nonspace = matchend(line, '\S', opening.pos + 1) - 1
        return nonspace > 0 ? nonspace : ind + s:sw()
      endif
    elseif closing.pos != -1
      call cursor(lnum, closing.pos + 1)
      keepjumps normal! %

      if crystal#indent#Match(line('.'), g:crystal#indent#crystal_indent_keywords)
        return indent('.') + s:sw()
      else
        return indent('.')
      endif
    else
      call cursor(clnum, vcol)
    end
  endif

  " If the previous line ended with an "end", match that "end"s beginning's
  " indent.
  let col = crystal#indent#Match(lnum, g:crystal#indent#end_end_regex)
  if col
    call cursor(lnum, col)
    if searchpair(
          \ g:crystal#indent#end_start_regex,
          \ g:crystal#indent#end_middle_regex,
          \ g:crystal#indent#end_end_regex,
          \ 'bW',
          \ g:crystal#indent#skip_expr)
      let n = line('.')
      let ind = indent('.')
      let msl = crystal#indent#GetMSL(n)
      if msl != n
        let ind = indent(msl)
      end
      return ind
    endif
  end

  let col = crystal#indent#Match(lnum, g:crystal#indent#crystal_indent_keywords)
  if col
    call cursor(lnum, col)
    let ind = virtcol('.') - 1 + s:sw()
    " TODO: make this better (we need to count them) (or, if a searchpair
    " fails, we know that something is lacking an end and thus we indent a
    " level
    if crystal#indent#Match(lnum, g:crystal#indent#end_end_regex)
      let ind = indent('.')
    endif
    return ind
  endif

  " Work on the MSL line. {{{2
  " ---------------------

  " Set up variables to use and search for MSL to the previous line.
  let p_lnum = lnum
  let lnum = crystal#indent#GetMSL(lnum)

  " If the previous line wasn't a MSL.
  if p_lnum != lnum
    " If previous line ends bracket and begins non-bracket continuation decrease indent by 1.
    if crystal#indent#Match(p_lnum, g:crystal#indent#bracket_switch_continuation_regex)
      return ind - 1
    " If previous line is a continuation return its indent.
    " TODO: the || crystal#indent#IsInString() thing worries me a bit.
    elseif crystal#indent#Match(p_lnum, g:crystal#indent#non_bracket_continuation_regex) ||
          \ crystal#indent#IsInString(p_lnum,strlen(line))
      return ind
    endif
  endif

  " Set up more variables, now that we know we wasn't continuation bound.
  let line = getline(lnum)
  let msl_ind = indent(lnum)

  " If the MSL line had an indenting keyword in it, add a level of indent.
  " TODO: this does not take into account contrived things such as
  " module Foo; class Bar; end
  if crystal#indent#Match(lnum, g:crystal#indent#crystal_indent_keywords)
    let ind = msl_ind + s:sw()
    if crystal#indent#Match(lnum, g:crystal#indent#end_end_regex)
      let ind = ind - s:sw()
    endif
    return ind
  endif

  " If the previous line ended with an operator -- but wasn't a block
  " ending, closing bracket, or type declaration -- indent one extra
  " level.
  if crystal#indent#Match(lnum, g:crystal#indent#non_bracket_continuation_regex) &&
        \ !crystal#indent#Match(lnum, '^\s*\([\])}]\|end\)')
    if lnum == p_lnum
      let ind = msl_ind + s:sw()
    else
      let ind = msl_ind
    endif
    return ind
  endif

  " }}}2

  return ind
endfunction

" }}}1

let &cpo = s:cpo_save
unlet s:cpo_save

" vim:set sw=2 sts=2 ts=8 et:

endif