blob: 1eaad09ef5c9b8796b3b9c0f2c192ae8fafddabf [file] [log] [blame]
Bram Moolenaarb529cfb2022-07-25 15:42:07 +01001" Support for Python indenting, see runtime/indent/python.vim
2
3let s:keepcpo= &cpo
4set cpo&vim
5
Bram Moolenaarfd999452022-08-24 18:30:14 +01006" need to inspect some old g:pyindent_* variables to be backward compatible
7let g:python_indent = extend(get(g:, 'python_indent', {}), #{
8 \ closed_paren_align_last_line: v:true,
9 \ open_paren: get(g:, 'pyindent_open_paren', 'shiftwidth() * 2'),
10 \ nested_paren: get(g:, 'pyindent_nested_paren', 'shiftwidth()'),
11 \ continue: get(g:, 'pyindent_continue', 'shiftwidth() * 2'),
12 "\ searchpair() can be slow, limit the time to 150 msec or what is put in
13 "\ g:python_indent.searchpair_timeout
14 \ searchpair_timeout: get(g:, 'pyindent_searchpair_timeout', 150),
15 "\ Identing inside parentheses can be very slow, regardless of the searchpair()
16 "\ timeout, so let the user disable this feature if he doesn't need it
17 \ disable_parentheses_indenting: get(g:, 'pyindent_disable_parentheses_indenting', v:false),
18 \ }, 'keep')
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +010019
20let s:maxoff = 50 " maximum number of lines to look backwards for ()
21
22function s:SearchBracket(fromlnum, flags)
23 return searchpairpos('[[({]', '', '[])}]', a:flags,
Bram Moolenaare8008642022-08-19 17:15:35 +010024 \ {-> synstack('.', col('.'))
25 \ ->map({_, id -> id->synIDattr('name')})
26 \ ->match('\%(Comment\|Todo\|String\)$') >= 0},
Bram Moolenaarfd999452022-08-24 18:30:14 +010027 \ [0, a:fromlnum - s:maxoff]->max(), g:python_indent.searchpair_timeout)
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +010028endfunction
29
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010030" See if the specified line is already user-dedented from the expected value.
31function s:Dedented(lnum, expected)
32 return indent(a:lnum) <= a:expected - shiftwidth()
33endfunction
34
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010035" Some other filetypes which embed Python have slightly different indent
36" rules (e.g. bitbake). Those filetypes can pass an extra funcref to this
37" function which is evaluated below.
38function python#GetIndent(lnum, ...)
39 let ExtraFunc = a:0 > 0 ? a:1 : 0
40
41 " If this line is explicitly joined: If the previous line was also joined,
42 " line it up with that one, otherwise add two 'shiftwidth'
43 if getline(a:lnum - 1) =~ '\\$'
44 if a:lnum > 1 && getline(a:lnum - 2) =~ '\\$'
45 return indent(a:lnum - 1)
46 endif
Bram Moolenaarfd999452022-08-24 18:30:14 +010047 return indent(a:lnum - 1) + get(g:, 'pyindent_continue', g:python_indent.continue)->eval()
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010048 endif
49
50 " If the start of the line is in a string don't change the indent.
51 if has('syntax_items')
52 \ && synIDattr(synID(a:lnum, 1, 1), "name") =~ "String$"
53 return -1
54 endif
55
56 " Search backwards for the previous non-empty line.
57 let plnum = prevnonblank(v:lnum - 1)
58
59 if plnum == 0
60 " This is the first non-empty line, use zero indent.
61 return 0
62 endif
63
Bram Moolenaarfd999452022-08-24 18:30:14 +010064 if g:python_indent.disable_parentheses_indenting == 1
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010065 let plindent = indent(plnum)
66 let plnumstart = plnum
67 else
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +010068 " Indent inside parens.
69 " Align with the open paren unless it is at the end of the line.
70 " E.g.
71 " open_paren_not_at_EOL(100,
72 " (200,
73 " 300),
74 " 400)
75 " open_paren_at_EOL(
76 " 100, 200, 300, 400)
77 call cursor(a:lnum, 1)
78 let [parlnum, parcol] = s:SearchBracket(a:lnum, 'nbW')
Bram Moolenaarfd999452022-08-24 18:30:14 +010079 if parlnum > 0
80 if parcol != col([parlnum, '$']) - 1
81 return parcol
82 elseif getline(a:lnum) =~ '^\s*[])}]' && !g:python_indent.closed_paren_align_last_line
83 return indent(parlnum)
84 endif
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +010085 endif
86
87 call cursor(plnum, 1)
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010088
89 " If the previous line is inside parenthesis, use the indent of the starting
90 " line.
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +010091 let [parlnum, _] = s:SearchBracket(plnum, 'nbW')
Bram Moolenaarb529cfb2022-07-25 15:42:07 +010092 if parlnum > 0
93 if a:0 > 0 && ExtraFunc(parlnum)
94 " We may have found the opening brace of a bitbake Python task, e.g. 'python do_task {'
95 " If so, ignore it here - it will be handled later.
96 let parlnum = 0
97 let plindent = indent(plnum)
98 let plnumstart = plnum
99 else
100 let plindent = indent(parlnum)
101 let plnumstart = parlnum
102 endif
103 else
104 let plindent = indent(plnum)
105 let plnumstart = plnum
106 endif
107
108 " When inside parenthesis: If at the first line below the parenthesis add
109 " two 'shiftwidth', otherwise same as previous line.
110 " i = (a
111 " + b
112 " + c)
113 call cursor(a:lnum, 1)
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +0100114 let [p, _] = s:SearchBracket(a:lnum, 'bW')
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100115 if p > 0
116 if a:0 > 0 && ExtraFunc(p)
117 " Currently only used by bitbake
118 " Handle first non-empty line inside a bitbake Python task
119 if p == plnum
120 return shiftwidth()
121 endif
122
123 " Handle the user actually trying to close a bitbake Python task
124 let line = getline(a:lnum)
125 if line =~ '^\s*}'
126 return -2
127 endif
128
129 " Otherwise ignore the brace
130 let p = 0
131 else
132 if p == plnum
133 " When the start is inside parenthesis, only indent one 'shiftwidth'.
Bram Moolenaar48c3f4e2022-08-08 15:42:38 +0100134 let [pp, _] = s:SearchBracket(a:lnum, 'bW')
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100135 if pp > 0
Bram Moolenaarfd999452022-08-24 18:30:14 +0100136 return indent(plnum)
137 \ + get(g:, 'pyindent_nested_paren', g:python_indent.nested_paren)->eval()
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100138 endif
Bram Moolenaarfd999452022-08-24 18:30:14 +0100139 return indent(plnum)
140 \ + get(g:, 'pyindent_open_paren', g:python_indent.open_paren)->eval()
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100141 endif
142 if plnumstart == p
143 return indent(plnum)
144 endif
145 return plindent
146 endif
147 endif
148 endif
149
150
151 " Get the line and remove a trailing comment.
152 " Use syntax highlighting attributes when possible.
153 let pline = getline(plnum)
154 let pline_len = strlen(pline)
155 if has('syntax_items')
156 " If the last character in the line is a comment, do a binary search for
157 " the start of the comment. synID() is slow, a linear search would take
158 " too long on a long line.
Bram Moolenaare8008642022-08-19 17:15:35 +0100159 if synstack(plnum, pline_len)
160 \ ->map({_, id -> id->synIDattr('name')})
161 \ ->match('\%(Comment\|Todo\)$') >= 0
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100162 let min = 1
163 let max = pline_len
164 while min < max
165 let col = (min + max) / 2
Bram Moolenaare8008642022-08-19 17:15:35 +0100166 if synstack(plnum, col)
167 \ ->map({_, id -> id->synIDattr('name')})
168 \ ->match('\%(Comment\|Todo\)$') >= 0
Bram Moolenaarb529cfb2022-07-25 15:42:07 +0100169 let max = col
170 else
171 let min = col + 1
172 endif
173 endwhile
174 let pline = strpart(pline, 0, min - 1)
175 endif
176 else
177 let col = 0
178 while col < pline_len
179 if pline[col] == '#'
180 let pline = strpart(pline, 0, col)
181 break
182 endif
183 let col = col + 1
184 endwhile
185 endif
186
187 " If the previous line ended with a colon, indent this line
188 if pline =~ ':\s*$'
189 return plindent + shiftwidth()
190 endif
191
192 " If the previous line was a stop-execution statement...
193 if getline(plnum) =~ '^\s*\(break\|continue\|raise\|return\|pass\)\>'
194 " See if the user has already dedented
195 if s:Dedented(a:lnum, indent(plnum))
196 " If so, trust the user
197 return -1
198 endif
199 " If not, recommend one dedent
200 return indent(plnum) - shiftwidth()
201 endif
202
203 " If the current line begins with a keyword that lines up with "try"
204 if getline(a:lnum) =~ '^\s*\(except\|finally\)\>'
205 let lnum = a:lnum - 1
206 while lnum >= 1
207 if getline(lnum) =~ '^\s*\(try\|except\)\>'
208 let ind = indent(lnum)
209 if ind >= indent(a:lnum)
210 return -1 " indent is already less than this
211 endif
212 return ind " line up with previous try or except
213 endif
214 let lnum = lnum - 1
215 endwhile
216 return -1 " no matching "try"!
217 endif
218
219 " If the current line begins with a header keyword, dedent
220 if getline(a:lnum) =~ '^\s*\(elif\|else\)\>'
221
222 " Unless the previous line was a one-liner
223 if getline(plnumstart) =~ '^\s*\(for\|if\|elif\|try\)\>'
224 return plindent
225 endif
226
227 " Or the user has already dedented
228 if s:Dedented(a:lnum, plindent)
229 return -1
230 endif
231
232 return plindent - shiftwidth()
233 endif
234
235 " When after a () construct we probably want to go back to the start line.
236 " a = (b
237 " + c)
238 " here
239 if parlnum > 0
240 " ...unless the user has already dedented
241 if s:Dedented(a:lnum, plindent)
242 return -1
243 else
244 return plindent
245 endif
246 endif
247
248 return -1
249endfunction
250
251let &cpo = s:keepcpo
252unlet s:keepcpo