Skip to content

Commit 24f2987

Browse files
committed
Indent embedded views
1 parent 527e6fd commit 24f2987

File tree

3 files changed

+342
-1
lines changed

3 files changed

+342
-1
lines changed

autoload/elixir/indent.vim

+101
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function! elixir#indent#indent(lnum)
2020
call cursor(lnum, 0)
2121

2222
let handlers = [
23+
\'inside_embedded_view',
2324
\'top_of_file',
2425
\'starts_with_string_continuation',
2526
\'following_trailing_binary_operator',
@@ -65,6 +66,17 @@ function! s:prev_starts_with(context, expr)
6566
return s:_starts_with(a:context.prev_nb_text, a:expr, a:context.prev_nb_lnum)
6667
endfunction
6768

69+
function! s:in_embedded_view()
70+
let groups = map(synstack(line('.'), col('.')), "synIDattr(v:val, 'name')")
71+
for group in ['elixirPhoenixESigil', 'elixirLiveViewSigil', 'elixirSurfaceSigil']
72+
if index(groups, group) >= 0
73+
return 1
74+
endif
75+
endfor
76+
77+
return 0
78+
endfunction
79+
6880
" Returns 0 or 1 based on whether or not the text starts with the given
6981
" expression and is not a string or comment
7082
function! s:_starts_with(text, expr, lnum)
@@ -156,6 +168,95 @@ function! s:find_last_pos(lnum, text, match)
156168
return -1
157169
endfunction
158170

171+
function! elixir#indent#handle_inside_embedded_view(context)
172+
if !s:in_embedded_view()
173+
return -1
174+
endif
175+
176+
" Multi-line Surface data delimiters
177+
let pair_lnum = searchpair('{{', '', '}}', 'bW', "line('.') == ".a:context.lnum." || s:is_string_or_comment(line('.'), col('.'))", max([0, a:context.lnum - g:elixir_indent_max_lookbehind]))
178+
if pair_lnum
179+
if a:context.text =~ '}}$'
180+
return -1
181+
else
182+
return indent(pair_lnum) + s:sw()
183+
endif
184+
endif
185+
186+
" Multi-line opening tag -- >, />, or %> are on a different line that their opening <
187+
let pair_lnum = searchpair('^\s\+<.*[^>]$', '', '^[^<]*[/%}]\?>$', 'bW', "line('.') == ".a:context.lnum." || s:is_string_or_comment(line('.'), col('.'))", max([0, a:context.lnum - g:elixir_indent_max_lookbehind]))
188+
if pair_lnum
189+
if a:context.text =~ '^\s\+\%\(>\|\/>\|%>\|}}>\)$'
190+
call s:debug("current line is a lone >, />, or %>")
191+
return indent(pair_lnum)
192+
elseif a:context.text =~ '\%\(>\|\/>\|%>\|}}>\)$'
193+
call s:debug("current line ends in >, />, or %>")
194+
if s:prev_ends_with(a:context, ',')
195+
return indent(a:context.prev_nb_lnum)
196+
else
197+
return -1
198+
endif
199+
else
200+
call s:debug("in the body of a multi-line opening tag")
201+
return indent(pair_lnum) + s:sw()
202+
endif
203+
endif
204+
205+
" Special cases
206+
if s:prev_ends_with(a:context, '^[^<]*do\s%>')
207+
call s:debug("prev line closes a multi-line do block")
208+
return indent(a:context.prev_nb_lnum)
209+
elseif a:context.prev_nb_text =~ 'do\s*%>$'
210+
call s:debug("prev line opens a do block")
211+
return indent(a:context.prev_nb_lnum) + s:sw()
212+
elseif a:context.text =~ '^\s\+<\/[a-zA-Z0-9\.\-_]\+>\|<% end %>'
213+
call s:debug("a single closing tag")
214+
if a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>.*<\/[a-zA-Z0-9\.\-_]\+>$'
215+
call s:debug("opening and closing tags are on the same line")
216+
return indent(a:context.prev_nb_lnum) - s:sw()
217+
elseif a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>\|\s\+>'
218+
call s:debug("prev line is opening html tag or single >")
219+
return indent(a:context.prev_nb_lnum)
220+
elseif s:prev_ends_with(a:context, '^[^<]*\%\(do\s\)\@<!%>')
221+
call s:debug("prev line closes a multi-line eex tag")
222+
return indent(a:context.prev_nb_lnum) - 2 * s:sw()
223+
else
224+
return indent(a:context.prev_nb_lnum) - s:sw()
225+
endif
226+
elseif a:context.text =~ '^\s*<%\s*\%(end\|else\|catch\|rescue\)\>.*%>'
227+
call s:debug("eex middle or closing eex tag")
228+
return indent(a:context.prev_nb_lnum) - s:sw()
229+
elseif a:context.prev_nb_text =~ '\s*<\/\|<% end %>$'
230+
call s:debug("prev is closing tag")
231+
return indent(a:context.prev_nb_lnum)
232+
elseif a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>.*<\/[a-zA-Z0-9\.\-_]\+>$'
233+
call s:debug("opening and closing tags are on the same line")
234+
return indent(a:context.prev_nb_lnum)
235+
elseif s:prev_ends_with(a:context, '\s\+\/>')
236+
call s:debug("prev ends with a single \>")
237+
return indent(a:context.prev_nb_lnum)
238+
elseif s:prev_ends_with(a:context, '^[^<]*\/>')
239+
call s:debug("prev line is closing a multi-line self-closing tag")
240+
return indent(a:context.prev_nb_lnum) - s:sw()
241+
elseif a:context.prev_nb_text =~ '^\s\+%\?>$'
242+
call s:debug("prev line is a single > or %>")
243+
return indent(a:context.prev_nb_lnum) + s:sw()
244+
endif
245+
246+
" Simple HTML (ie, opening tag is not split across lines)
247+
let pair_lnum = searchpair('^\s\+<[^%\/].*[^\/>]>$', '', '^\s\+<\/\w\+>$', 'bW', "line('.') == ".a:context.lnum." || s:is_string_or_comment(line('.'), col('.'))", max([0, a:context.lnum - g:elixir_indent_max_lookbehind]))
248+
if pair_lnum
249+
call s:debug("simple HTML")
250+
if a:context.text =~ '^\s\+<\/\w\+>$'
251+
return indent(pair_lnum)
252+
else
253+
return indent(pair_lnum) + s:sw()
254+
endif
255+
endif
256+
257+
return -1
258+
endfunction
259+
159260
function! elixir#indent#handle_top_of_file(context)
160261
if a:context.prev_nb_lnum == 0
161262
return 0

indent/elixir.vim

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let b:did_indent = 1
66
setlocal indentexpr=elixir#indent(v:lnum)
77

88
setlocal indentkeys+==after,=catch,=do,=else,=end,=rescue,
9-
setlocal indentkeys+=*<Return>,=->,=\|>,=<>,0},0],0)
9+
setlocal indentkeys+=*<Return>,=->,=\|>,=<>,0},0],0),>
1010

1111
" TODO: @jbodah 2017-02-27: all operators should cause reindent when typed
1212

spec/indent/embedded_views_spec.rb

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'Indenting embedded views' do
6+
i <<~EOF
7+
def render(assigns) do
8+
~L"""
9+
<div>
10+
Some content
11+
</div>
12+
"""
13+
end
14+
EOF
15+
16+
i <<~EOF
17+
def render(assigns) do
18+
~H"""
19+
<div class="theres a-/ in the class names from tailwind">
20+
<div class="some more classes">
21+
This is immediately nested
22+
</div>
23+
</div>
24+
"""
25+
end
26+
EOF
27+
28+
i <<~EOF
29+
def render(assigns) do
30+
~L"""
31+
<div id="123456">
32+
Some content
33+
</div>
34+
"""
35+
end
36+
EOF
37+
38+
i <<~EOF
39+
def render(assigns) do
40+
~L"""
41+
<div
42+
id="123456"
43+
>
44+
Some content
45+
</div>
46+
"""
47+
end
48+
EOF
49+
50+
i <<~EOF
51+
def render(assigns) do
52+
~L"""
53+
<div />
54+
<p>Some paragraph</p>
55+
"""
56+
end
57+
EOF
58+
59+
i <<~EOF
60+
def render(assigns) do
61+
~L"""
62+
<div>
63+
it
64+
<div>
65+
keeps
66+
<div>
67+
nesting
68+
</div>
69+
</div>
70+
</div>
71+
"""
72+
end
73+
EOF
74+
75+
i <<~EOF
76+
def render(assgins) do
77+
~L"""
78+
<div>
79+
<%= for i <- iter do %>
80+
<div><%= i %></div>
81+
<% end %>
82+
</div>
83+
"""
84+
end
85+
EOF
86+
87+
i <<~EOF
88+
def render(assigns) do
89+
~L"""
90+
<%= live_component @socket,
91+
Component,
92+
id: "<%= @id %>",
93+
user: @user do
94+
%>
95+
96+
<main>
97+
<header>
98+
<h1>Some Header</h1>
99+
</header>
100+
<section>
101+
<h1>Some Section</h1>
102+
<p>
103+
I'm some text
104+
</p>
105+
</section>
106+
</main>
107+
108+
<% end %>
109+
"""
110+
end
111+
EOF
112+
113+
i <<~EOF
114+
def render(assigns) do
115+
~L"""
116+
<%= render_component,
117+
@socket,
118+
Component do %>
119+
120+
<p>Multi-line opening eex tag that takes a block</p>
121+
<% end %>
122+
"""
123+
end
124+
EOF
125+
126+
i <<~EOF
127+
def render(assigns) do
128+
~L"""
129+
<div>
130+
<%= render_component,
131+
@socket,
132+
Component %>
133+
</div>
134+
135+
<%= render_component,
136+
@socket,
137+
Component %>
138+
<p>Multi-line single eex tag</p>
139+
"""
140+
end
141+
EOF
142+
143+
i <<~EOF
144+
def render(assigns) do
145+
~L"""
146+
<%= live_component @socket,
147+
Component,
148+
id: "<%= @id %>",
149+
team: @team do
150+
%>
151+
152+
<div>
153+
<div>
154+
<div>
155+
A deeply nested tree
156+
<div>
157+
with trailing whitespace
158+
159+
</div>
160+
</div>
161+
</div>
162+
</div>
163+
164+
<div id="id-ends-with-greater-than->"
165+
propWithEexTag="<%= @id %>"
166+
anotherProp="foo"
167+
/>
168+
169+
<%= for i <- iter do %>
170+
<div><%= i %></div>
171+
<% end %>
172+
173+
<div
174+
opts={{
175+
opt1: "optA",
176+
opt2: "optB"
177+
}}
178+
id="hi"
179+
bye="hi" />
180+
181+
<ul>
182+
<li :for={{ item <- @items }}>
183+
{{ item }}
184+
</li>
185+
</ul>
186+
187+
<div id="hi">
188+
Hi <p>hi</p>
189+
I'm ok, ok?
190+
<div>
191+
hi there!
192+
</div>
193+
<div>
194+
<div>
195+
<p>hi</p>
196+
<hr />
197+
</div>
198+
</div>
199+
</div>
200+
201+
<Some.Surface.Component />
202+
203+
<Another
204+
prop="prop"
205+
prop2="prop2"
206+
>
207+
<div>content</div>
208+
</Another>
209+
210+
<div foo />
211+
212+
<div>hi</div>
213+
214+
<div>
215+
<div>
216+
content
217+
</div>
218+
<div />
219+
<div>
220+
content in new div after a self-closing div
221+
</div>
222+
</div>
223+
224+
<p
225+
id="<%= @id %>"
226+
class="multi-line opening single letter p tag"
227+
>
228+
<%= @solo.eex_tag %>
229+
<Nested
230+
prop="nested"
231+
>
232+
content
233+
</Nested>
234+
</p>
235+
236+
<% end %>
237+
"""
238+
end
239+
EOF
240+
end

0 commit comments

Comments
 (0)