]>
Commit | Line | Data |
---|---|---|
0d23b6e5 BB |
1 | " Language: CoffeeScript |
2 | " Maintainer: Mick Koch <kchmck@gmail.com> | |
3 | " URL: http://github.com/kchmck/vim-coffee-script | |
4 | " License: WTFPL | |
5 | ||
6 | if exists("b:did_indent") | |
7 | finish | |
8 | endif | |
9 | ||
10 | let b:did_indent = 1 | |
11 | ||
12 | setlocal autoindent | |
13 | setlocal indentexpr=GetCoffeeIndent(v:lnum) | |
14 | " Make sure GetCoffeeIndent is run when these are typed so they can be | |
15 | " indented or outdented. | |
16 | setlocal indentkeys+=0],0),0.,=else,=when,=catch,=finally | |
17 | ||
18 | " Only define the function once. | |
19 | if exists("*GetCoffeeIndent") | |
20 | finish | |
21 | endif | |
22 | ||
23 | " Keywords to indent after | |
24 | let s:INDENT_AFTER_KEYWORD = '^\%(if\|unless\|else\|for\|while\|until\|' | |
25 | \ . 'loop\|switch\|when\|try\|catch\|finally\|' | |
26 | \ . 'class\)\>' | |
27 | ||
28 | " Operators to indent after | |
29 | let s:INDENT_AFTER_OPERATOR = '\%([([{:=]\|[-=]>\)$' | |
30 | ||
31 | " Keywords and operators that continue a line | |
32 | let s:CONTINUATION = '\<\%(is\|isnt\|and\|or\)\>$' | |
33 | \ . '\|' | |
34 | \ . '\%(-\@<!-\|+\@<!+\|<\|[-=]\@<!>\|\*\|/\@<!/\|%\||\|' | |
35 | \ . '&\|,\|\.\@<!\.\)$' | |
36 | ||
37 | " Operators that block continuation indenting | |
38 | let s:CONTINUATION_BLOCK = '[([{:=]$' | |
39 | ||
40 | " A continuation dot access | |
41 | let s:DOT_ACCESS = '^\.' | |
42 | ||
43 | " Keywords to outdent after | |
44 | let s:OUTDENT_AFTER = '^\%(return\|break\|continue\|throw\)\>' | |
45 | ||
46 | " A compound assignment like `... = if ...` | |
47 | let s:COMPOUND_ASSIGNMENT = '[:=]\s*\%(if\|unless\|for\|while\|until\|' | |
48 | \ . 'switch\|try\|class\)\>' | |
49 | ||
50 | " A postfix condition like `return ... if ...`. | |
51 | let s:POSTFIX_CONDITION = '\S\s\+\zs\<\%(if\|unless\)\>' | |
52 | ||
53 | " A single-line else statement like `else ...` but not `else if ... | |
54 | let s:SINGLE_LINE_ELSE = '^else\s\+\%(\<\%(if\|unless\)\>\)\@!' | |
55 | ||
56 | " Max lines to look back for a match | |
57 | let s:MAX_LOOKBACK = 50 | |
58 | ||
59 | " Syntax names for strings | |
60 | let s:SYNTAX_STRING = 'coffee\%(String\|AssignString\|Embed\|Regex\|Heregex\|' | |
61 | \ . 'Heredoc\)' | |
62 | ||
63 | " Syntax names for comments | |
64 | let s:SYNTAX_COMMENT = 'coffee\%(Comment\|BlockComment\|HeregexComment\)' | |
65 | ||
66 | " Syntax names for strings and comments | |
67 | let s:SYNTAX_STRING_COMMENT = s:SYNTAX_STRING . '\|' . s:SYNTAX_COMMENT | |
68 | ||
69 | " Get the linked syntax name of a character. | |
70 | function! s:SyntaxName(linenum, col) | |
71 | return synIDattr(synID(a:linenum, a:col, 1), 'name') | |
72 | endfunction | |
73 | ||
74 | " Check if a character is in a comment. | |
75 | function! s:IsComment(linenum, col) | |
76 | return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_COMMENT | |
77 | endfunction | |
78 | ||
79 | " Check if a character is in a string. | |
80 | function! s:IsString(linenum, col) | |
81 | return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING | |
82 | endfunction | |
83 | ||
84 | " Check if a character is in a comment or string. | |
85 | function! s:IsCommentOrString(linenum, col) | |
86 | return s:SyntaxName(a:linenum, a:col) =~ s:SYNTAX_STRING_COMMENT | |
87 | endfunction | |
88 | ||
89 | " Check if a whole line is a comment. | |
90 | function! s:IsCommentLine(linenum) | |
91 | " Check the first non-whitespace character. | |
92 | return s:IsComment(a:linenum, indent(a:linenum) + 1) | |
93 | endfunction | |
94 | ||
95 | " Repeatedly search a line for a regex until one is found outside a string or | |
96 | " comment. | |
97 | function! s:SmartSearch(linenum, regex) | |
98 | " Start at the first column. | |
99 | let col = 0 | |
100 | ||
101 | " Search until there are no more matches, unless a good match is found. | |
102 | while 1 | |
103 | call cursor(a:linenum, col + 1) | |
104 | let [_, col] = searchpos(a:regex, 'cn', a:linenum) | |
105 | ||
106 | " No more matches. | |
107 | if !col | |
108 | break | |
109 | endif | |
110 | ||
111 | if !s:IsCommentOrString(a:linenum, col) | |
112 | return 1 | |
113 | endif | |
114 | endwhile | |
115 | ||
116 | " No good match found. | |
117 | return 0 | |
118 | endfunction | |
119 | ||
120 | " Skip a match if it's in a comment or string, is a single-line statement that | |
121 | " isn't adjacent, or is a postfix condition. | |
122 | function! s:ShouldSkip(startlinenum, linenum, col) | |
123 | if s:IsCommentOrString(a:linenum, a:col) | |
124 | return 1 | |
125 | endif | |
126 | ||
127 | " Check for a single-line statement that isn't adjacent. | |
128 | if s:SmartSearch(a:linenum, '\<then\>') && a:startlinenum - a:linenum > 1 | |
129 | return 1 | |
130 | endif | |
131 | ||
132 | if s:SmartSearch(a:linenum, s:POSTFIX_CONDITION) && | |
133 | \ !s:SmartSearch(a:linenum, s:COMPOUND_ASSIGNMENT) | |
134 | return 1 | |
135 | endif | |
136 | ||
137 | return 0 | |
138 | endfunction | |
139 | ||
140 | " Find the farthest line to look back to, capped to line 1 (zero and negative | |
141 | " numbers cause bad things). | |
142 | function! s:MaxLookback(startlinenum) | |
143 | return max([1, a:startlinenum - s:MAX_LOOKBACK]) | |
144 | endfunction | |
145 | ||
146 | " Get the skip expression for searchpair(). | |
147 | function! s:SkipExpr(startlinenum) | |
148 | return "s:ShouldSkip(" . a:startlinenum . ", line('.'), col('.'))" | |
149 | endfunction | |
150 | ||
151 | " Search for pairs of text. | |
152 | function! s:SearchPair(start, end) | |
153 | " The cursor must be in the first column for regexes to match. | |
154 | call cursor(0, 1) | |
155 | ||
156 | let startlinenum = line('.') | |
157 | ||
158 | " Don't need the W flag since MaxLookback caps the search to line 1. | |
159 | return searchpair(a:start, '', a:end, 'bcn', | |
160 | \ s:SkipExpr(startlinenum), | |
161 | \ s:MaxLookback(startlinenum)) | |
162 | endfunction | |
163 | ||
164 | " Try to find a previous matching line. | |
165 | function! s:GetMatch(curline) | |
166 | let firstchar = a:curline[0] | |
167 | ||
168 | if firstchar == '}' | |
169 | return s:SearchPair('{', '}') | |
170 | elseif firstchar == ')' | |
171 | return s:SearchPair('(', ')') | |
172 | elseif firstchar == ']' | |
173 | return s:SearchPair('\[', '\]') | |
174 | elseif a:curline =~ '^else\>' | |
175 | return s:SearchPair('\<\%(if\|unless\|when\)\>', '\<else\>') | |
176 | elseif a:curline =~ '^catch\>' | |
177 | return s:SearchPair('\<try\>', '\<catch\>') | |
178 | elseif a:curline =~ '^finally\>' | |
179 | return s:SearchPair('\<try\>', '\<finally\>') | |
180 | endif | |
181 | ||
182 | return 0 | |
183 | endfunction | |
184 | ||
185 | " Get the nearest previous line that isn't a comment. | |
186 | function! s:GetPrevNormalLine(startlinenum) | |
187 | let curlinenum = a:startlinenum | |
188 | ||
189 | while curlinenum > 0 | |
190 | let curlinenum = prevnonblank(curlinenum - 1) | |
191 | ||
192 | if !s:IsCommentLine(curlinenum) | |
193 | return curlinenum | |
194 | endif | |
195 | endwhile | |
196 | ||
197 | return 0 | |
198 | endfunction | |
199 | ||
200 | " Try to find a comment in a line. | |
201 | function! s:FindComment(linenum) | |
202 | let col = 0 | |
203 | ||
204 | while 1 | |
205 | call cursor(a:linenum, col + 1) | |
206 | let [_, col] = searchpos('#', 'cn', a:linenum) | |
207 | ||
208 | if !col | |
209 | break | |
210 | endif | |
211 | ||
212 | if s:IsComment(a:linenum, col) | |
213 | return col | |
214 | endif | |
215 | endwhile | |
216 | ||
217 | return 0 | |
218 | endfunction | |
219 | ||
220 | " Get a line without comments or surrounding whitespace. | |
221 | function! s:GetTrimmedLine(linenum) | |
222 | let comment = s:FindComment(a:linenum) | |
223 | let line = getline(a:linenum) | |
224 | ||
225 | if comment | |
226 | " Subtract 1 to get to the column before the comment and another 1 for | |
227 | " zero-based indexing. | |
228 | let line = line[:comment - 2] | |
229 | endif | |
230 | ||
231 | return substitute(substitute(line, '^\s\+', '', ''), | |
232 | \ '\s\+$', '', '') | |
233 | endfunction | |
234 | ||
235 | function! s:GetCoffeeIndent(curlinenum) | |
236 | let prevlinenum = s:GetPrevNormalLine(a:curlinenum) | |
237 | ||
238 | " Don't do anything if there's no previous line. | |
239 | if !prevlinenum | |
240 | return -1 | |
241 | endif | |
242 | ||
243 | let curline = s:GetTrimmedLine(a:curlinenum) | |
244 | ||
245 | " Try to find a previous matching statement. This handles outdenting. | |
246 | let matchlinenum = s:GetMatch(curline) | |
247 | ||
248 | if matchlinenum | |
249 | return indent(matchlinenum) | |
250 | endif | |
251 | ||
252 | " Try to find a matching `when`. | |
253 | if curline =~ '^when\>' && !s:SmartSearch(prevlinenum, '\<switch\>') | |
254 | let linenum = a:curlinenum | |
255 | ||
256 | while linenum > 0 | |
257 | let linenum = s:GetPrevNormalLine(linenum) | |
258 | ||
259 | if getline(linenum) =~ '^\s*when\>' | |
260 | return indent(linenum) | |
261 | endif | |
262 | endwhile | |
263 | ||
264 | return -1 | |
265 | endif | |
266 | ||
267 | let prevline = s:GetTrimmedLine(prevlinenum) | |
268 | let previndent = indent(prevlinenum) | |
269 | ||
270 | " Always indent after these operators. | |
271 | if prevline =~ s:INDENT_AFTER_OPERATOR | |
272 | return previndent + &shiftwidth | |
273 | endif | |
274 | ||
275 | " Indent after a continuation if it's the first. | |
276 | if prevline =~ s:CONTINUATION | |
277 | let prevprevlinenum = s:GetPrevNormalLine(prevlinenum) | |
278 | let prevprevline = s:GetTrimmedLine(prevprevlinenum) | |
279 | ||
280 | if prevprevline !~ s:CONTINUATION && prevprevline !~ s:CONTINUATION_BLOCK | |
281 | return previndent + &shiftwidth | |
282 | endif | |
283 | ||
284 | return -1 | |
285 | endif | |
286 | ||
287 | " Indent after these keywords and compound assignments if they aren't a | |
288 | " single-line statement. | |
289 | if prevline =~ s:INDENT_AFTER_KEYWORD || prevline =~ s:COMPOUND_ASSIGNMENT | |
290 | if !s:SmartSearch(prevlinenum, '\<then\>') && prevline !~ s:SINGLE_LINE_ELSE | |
291 | return previndent + &shiftwidth | |
292 | endif | |
293 | ||
294 | return -1 | |
295 | endif | |
296 | ||
297 | " Indent a dot access if it's the first. | |
298 | if curline =~ s:DOT_ACCESS && prevline !~ s:DOT_ACCESS | |
299 | return previndent + &shiftwidth | |
300 | endif | |
301 | ||
302 | " Outdent after these keywords if they don't have a postfix condition or are | |
303 | " a single-line statement. | |
304 | if prevline =~ s:OUTDENT_AFTER | |
305 | if !s:SmartSearch(prevlinenum, s:POSTFIX_CONDITION) || | |
306 | \ s:SmartSearch(prevlinenum, '\<then\>') | |
307 | return previndent - &shiftwidth | |
308 | endif | |
309 | endif | |
310 | ||
311 | " No indenting or outdenting is needed. | |
312 | return -1 | |
313 | endfunction | |
314 | ||
315 | " Wrap s:GetCoffeeIndent to keep the cursor position. | |
316 | function! GetCoffeeIndent(curlinenum) | |
317 | let oldcursor = getpos('.') | |
318 | let indent = s:GetCoffeeIndent(a:curlinenum) | |
319 | call setpos('.', oldcursor) | |
320 | ||
321 | return indent | |
322 | endfunction |