summaryrefslogtreecommitdiffstats
path: root/runtime/indent/rust.vim
blob: 7c055ec73942ea5251cd9b2b24103cf3bba58126 (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
" Vim indent file
" Language:         Rust
" Author:           Chris Morgan <me@chrismorgan.info>
" Last Change:      2023-09-11
" For bugs, patches and license go to https://github.com/rust-lang/rust.vim

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

setlocal cindent
setlocal cinoptions=L0,(s,Ws,J1,j1,m1
setlocal cinkeys=0{,0},!^F,o,O,0[,0],0(,0)
" Don't think cinwords will actually do anything at all... never mind
setlocal cinwords=for,if,else,while,loop,impl,mod,unsafe,trait,struct,enum,fn,extern,macro

" Some preliminary settings
setlocal nolisp		" Make sure lisp indenting doesn't supersede us
setlocal autoindent	" indentexpr isn't much help otherwise
" Also do indentkeys, otherwise # gets shoved to column 0 :-/
setlocal indentkeys=0{,0},!^F,o,O,0[,0],0(,0)

setlocal indentexpr=GetRustIndent(v:lnum)

let b:undo_indent = "setlocal cindent< cinoptions< cinkeys< cinwords< lisp< autoindent< indentkeys< indentexpr<"

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

" vint: -ProhibitAbbreviationOption
let s:save_cpo = &cpo
set cpo&vim
" vint: +ProhibitAbbreviationOption

" Come here when loading the script the first time.

function! s:get_line_trimmed(lnum)
    " Get the line and remove a trailing comment.
    " Use syntax highlighting attributes when possible.
    " NOTE: this is not accurate; /* */ or a line continuation could trick it
    let line = getline(a:lnum)
    let line_len = strlen(line)
    if has('syntax_items')
        " If the last character in the line is a comment, do a binary search for
        " the start of the comment.  synID() is slow, a linear search would take
        " too long on a long line.
        if synIDattr(synID(a:lnum, line_len, 1), "name") =~? 'Comment\|Todo'
            let min = 1
            let max = line_len
            while min < max
                let col = (min + max) / 2
                if synIDattr(synID(a:lnum, col, 1), "name") =~? 'Comment\|Todo'
                    let max = col
                else
                    let min = col + 1
                endif
            endwhile
            let line = strpart(line, 0, min - 1)
        endif
        return substitute(line, "\s*$", "", "")
    else
        " Sorry, this is not complete, nor fully correct (e.g. string "//").
        " Such is life.
        return substitute(line, "\s*//.*$", "", "")
    endif
endfunction

function! s:is_string_comment(lnum, col)
    if has('syntax_items')
        for id in synstack(a:lnum, a:col)
            let synname = synIDattr(id, "name")
            if synname ==# "rustString" || synname =~# "^rustComment"
                return 1
            endif
        endfor
    else
        " without syntax, let's not even try
        return 0
    endif
endfunction

if exists('*shiftwidth')
    function! s:shiftwidth()
        return shiftwidth()
    endfunc
else
    function! s:shiftwidth()
        return &shiftwidth
    endfunc
endif

function GetRustIndent(lnum)
    " Starting assumption: cindent (called at the end) will do it right
    " normally. We just want to fix up a few cases.

    let line = getline(a:lnum)

    if has('syntax_items')
        let synname = synIDattr(synID(a:lnum, 1, 1), "name")
        if synname ==# "rustString"
            " If the start of the line is in a string, don't change the indent
            return -1
        elseif synname =~? '\(Comment\|Todo\)'
                    \ && line !~# '^\s*/\*'  " not /* opening line
            if synname =~? "CommentML" " multi-line
                if line !~# '^\s*\*' && getline(a:lnum - 1) =~# '^\s*/\*'
                    " This is (hopefully) the line after a /*, and it has no
                    " leader, so the correct indentation is that of the
                    " previous line.
                    return GetRustIndent(a:lnum - 1)
                endif
            endif
            " If it's in a comment, let cindent take care of it now. This is
            " for cases like "/*" where the next line should start " * ", not
            " "* " as the code below would otherwise cause for module scope
            " Fun fact: "  /*\n*\n*/" takes two calls to get right!
            return cindent(a:lnum)
        endif
    endif

    " cindent gets second and subsequent match patterns/struct members wrong,
    " as it treats the comma as indicating an unfinished statement::
    "
    " match a {
    "     b => c,
    "         d => e,
    "         f => g,
    " };

    " Search backwards for the previous non-empty line.
    let prevlinenum = prevnonblank(a:lnum - 1)
    let prevline = s:get_line_trimmed(prevlinenum)
    while prevlinenum > 1 && prevline !~# '[^[:blank:]]'
        let prevlinenum = prevnonblank(prevlinenum - 1)
        let prevline = s:get_line_trimmed(prevlinenum)
    endwhile

    " A standalone '{', '}', or 'where'
    let l:standalone_open = line =~# '\V\^\s\*{\s\*\$'
    let l:standalone_close = line =~# '\V\^\s\*}\s\*\$'
    let l:standalone_where = line =~# '\V\^\s\*where\s\*\$'
    if l:standalone_open || l:standalone_close || l:standalone_where
        " ToDo: we can search for more items than 'fn' and 'if'.
        let [l:found_line, l:col, l:submatch] =
                    \ searchpos('\<\(fn\)\|\(if\)\>', 'bnWp')
        if l:found_line !=# 0
            " Now we count the number of '{' and '}' in between the match
            " locations and the current line (there is probably a better
            " way to compute this).
            let l:i = l:found_line
            let l:search_line = strpart(getline(l:i), l:col - 1)
            let l:opens = 0
            let l:closes = 0
            while l:i < a:lnum
                let l:search_line2 = substitute(l:search_line, '\V{', '', 'g')
                let l:opens += strlen(l:search_line) - strlen(l:search_line2)
                let l:search_line3 = substitute(l:search_line2, '\V}', '', 'g')
                let l:closes += strlen(l:search_line2) - strlen(l:search_line3)
                let l:i += 1
                let l:search_line = getline(l:i)
            endwhile
            if l:standalone_open || l:standalone_where
                if l:opens ==# l:closes
                    return indent(l:found_line)
                endif
            else
                " Expect to find just one more close than an open
                if l:opens ==# l:closes + 1
                    return indent(l:found_line)
                endif
            endif
        endif
    endif

    " A standalone 'where' adds a shift.
    let l:standalone_prevline_where = prevline =~# '\V\^\s\*where\s\*\$'
    if l:standalone_prevline_where
        return indent(prevlinenum) + 4
    endif

    " Handle where clauses nicely: subsequent values should line up nicely.
    if prevline[len(prevline) - 1] ==# ","
                \ && prevline =~# '^\s*where\s'
        return indent(prevlinenum) + 6
    endif

    let l:last_prevline_character = prevline[len(prevline) - 1]

    " A line that ends with '.<expr>;' is probably an end of a long list
    " of method operations.
    if prevline =~# '\V\^\s\*.' && l:last_prevline_character ==# ';'
        call cursor(a:lnum - 1, 1)
        let l:scope_start = searchpair('{\|(', '', '}\|)', 'nbW',
                    \ 's:is_string_comment(line("."), col("."))')
        if l:scope_start != 0 && l:scope_start < a:lnum
            return indent(l:scope_start) + 4
        endif
    endif

    if l:last_prevline_character ==# ","
                \ && s:get_line_trimmed(a:lnum) !~# '^\s*[\[\]{})]'
                \ && prevline !~# '^\s*fn\s'
                \ && prevline !~# '([^()]\+,$'
                \ && s:get_line_trimmed(a:lnum) !~# '^\s*\S\+\s*=>'
        " Oh ho! The previous line ended in a comma! I bet cindent will try to
        " take this too far... For now, let's normally use the previous line's
        " indent.

        " One case where this doesn't work out is where *this* line contains
        " square or curly brackets; then we normally *do* want to be indenting
        " further.
        "
        " Another case where we don't want to is one like a function
        " definition with arguments spread over multiple lines:
        "
        " fn foo(baz: Baz,
        "        baz: Baz) // <-- cindent gets this right by itself
        "
        " Another case is similar to the previous, except calling a function
        " instead of defining it, or any conditional expression that leaves
        " an open paren:
        "
        " foo(baz,
        "     baz);
        "
        " if baz && (foo ||
        "            bar) {
        "
        " Another case is when the current line is a new match arm.
        "
        " There are probably other cases where we don't want to do this as
        " well. Add them as needed.
        return indent(prevlinenum)
    endif

    if !has("patch-7.4.355")
        " cindent before 7.4.355 doesn't do the module scope well at all; e.g.::
        "
        " static FOO : &'static [bool] = [
        " true,
        "	 false,
        "	 false,
        "	 true,
        "	 ];
        "
        "	 uh oh, next statement is indented further!

        " Note that this does *not* apply the line continuation pattern properly;
        " that's too hard to do correctly for my liking at present, so I'll just
        " start with these two main cases (square brackets and not returning to
        " column zero)

        call cursor(a:lnum, 1)
        if searchpair('{\|(', '', '}\|)', 'nbW',
                    \ 's:is_string_comment(line("."), col("."))') == 0
            if searchpair('\[', '', '\]', 'nbW',
                        \ 's:is_string_comment(line("."), col("."))') == 0
                " Global scope, should be zero
                return 0
            else
                " At the module scope, inside square brackets only
                "if getline(a:lnum)[0] == ']' || search('\[', '', '\]', 'nW') == a:lnum
                if line =~# "^\\s*]"
                    " It's the closing line, dedent it
                    return 0
                else
                    return &shiftwidth
                endif
            endif
        endif
    endif

    " Fall back on cindent, which does it mostly right
    return cindent(a:lnum)
endfunction

" vint: -ProhibitAbbreviationOption
let &cpo = s:save_cpo
unlet s:save_cpo
" vint: +ProhibitAbbreviationOption

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