diff --git a/autoload/elixir/indent.vim b/autoload/elixir/indent.vim index 34050f19..66114773 100644 --- a/autoload/elixir/indent.vim +++ b/autoload/elixir/indent.vim @@ -20,6 +20,7 @@ function! elixir#indent#indent(lnum) call cursor(lnum, 0) let handlers = [ + \'inside_embedded_view', \'top_of_file', \'starts_with_string_continuation', \'following_trailing_binary_operator', @@ -65,6 +66,17 @@ function! s:prev_starts_with(context, expr) return s:_starts_with(a:context.prev_nb_text, a:expr, a:context.prev_nb_lnum) endfunction +function! s:in_embedded_view() + let groups = map(synstack(line('.'), col('.')), "synIDattr(v:val, 'name')") + for group in ['elixirPhoenixESigil', 'elixirLiveViewSigil', 'elixirSurfaceSigil'] + if index(groups, group) >= 0 + return 1 + endif + endfor + + return 0 +endfunction + " Returns 0 or 1 based on whether or not the text starts with the given " expression and is not a string or comment function! s:_starts_with(text, expr, lnum) @@ -156,6 +168,104 @@ function! s:find_last_pos(lnum, text, match) return -1 endfunction +function! elixir#indent#handle_inside_embedded_view(context) + if !s:in_embedded_view() + return -1 + endif + + " Multi-line Surface data delimiters + 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])) + if pair_lnum + if a:context.text =~ '}}$' + return indent(pair_lnum) + elseif a:context.text =~ '}}*>$' + return -1 + elseif s:prev_ends_with(a:context, '[\|%{') + return indent(a:context.prev_nb_lnum) + s:sw() + elseif a:context.prev_nb_text =~ ',$' + return indent(a:context.prev_nb_lnum) + else + return indent(pair_lnum) + s:sw() + endif + endif + + " Multi-line opening tag -- >, />, or %> are on a different line that their opening < + 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])) + if pair_lnum + if a:context.text =~ '^\s\+\%\(>\|\/>\|%>\|}}>\)$' + call s:debug("current line is a lone >, />, or %>") + return indent(pair_lnum) + elseif a:context.text =~ '\%\(>\|\/>\|%>\|}}>\)$' + call s:debug("current line ends in >, />, or %>") + if s:prev_ends_with(a:context, ',') + return indent(a:context.prev_nb_lnum) + else + return -1 + endif + else + call s:debug("in the body of a multi-line opening tag") + return indent(pair_lnum) + s:sw() + endif + endif + + " Special cases + if s:prev_ends_with(a:context, '^[^<]*do\s%>') + call s:debug("prev line closes a multi-line do block") + return indent(a:context.prev_nb_lnum) + elseif a:context.prev_nb_text =~ 'do\s*%>$' + call s:debug("prev line opens a do block") + return indent(a:context.prev_nb_lnum) + s:sw() + elseif a:context.text =~ '^\s\+<\/[a-zA-Z0-9\.\-_]\+>\|<% end %>' + call s:debug("a single closing tag") + if a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>.*<\/[a-zA-Z0-9\.\-_]\+>$' + call s:debug("opening and closing tags are on the same line") + return indent(a:context.prev_nb_lnum) - s:sw() + elseif a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>\|\s\+>' + call s:debug("prev line is opening html tag or single >") + return indent(a:context.prev_nb_lnum) + elseif s:prev_ends_with(a:context, '^[^<]*\%\(do\s\)\@') + call s:debug("prev line closes a multi-line eex tag") + return indent(a:context.prev_nb_lnum) - 2 * s:sw() + else + return indent(a:context.prev_nb_lnum) - s:sw() + endif + elseif a:context.text =~ '^\s*<%\s*\%(end\|else\|catch\|rescue\)\>.*%>' + call s:debug("eex middle or closing eex tag") + return indent(a:context.prev_nb_lnum) - s:sw() + elseif a:context.prev_nb_text =~ '\s*<\/\|<% end %>$' + call s:debug("prev is closing tag") + return indent(a:context.prev_nb_lnum) + elseif a:context.prev_nb_text =~ '^\s\+<[^%\/]*[^/]>.*<\/[a-zA-Z0-9\.\-_]\+>$' + call s:debug("opening and closing tags are on the same line") + return indent(a:context.prev_nb_lnum) + elseif s:prev_ends_with(a:context, '\s\+\/>') + call s:debug("prev ends with a single \>") + return indent(a:context.prev_nb_lnum) + elseif s:prev_ends_with(a:context, '^[^<]*\/>') + call s:debug("prev line is closing a multi-line self-closing tag") + return indent(a:context.prev_nb_lnum) - s:sw() + elseif s:prev_ends_with(a:context, '^\s\+<.*\/>') + call s:debug("prev line is closing self-closing tag") + return indent(a:context.prev_nb_lnum) + elseif a:context.prev_nb_text =~ '^\s\+%\?>$' + call s:debug("prev line is a single > or %>") + return indent(a:context.prev_nb_lnum) + s:sw() + endif + + " Simple HTML (ie, opening tag is not split across lines) + 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])) + if pair_lnum + call s:debug("simple HTML") + if a:context.text =~ '^\s\+<\/\w\+>$' + return indent(pair_lnum) + else + return indent(pair_lnum) + s:sw() + endif + endif + + return -1 +endfunction + function! elixir#indent#handle_top_of_file(context) if a:context.prev_nb_lnum == 0 return 0 diff --git a/indent/elixir.vim b/indent/elixir.vim index 31e7f8fa..13c97e3c 100644 --- a/indent/elixir.vim +++ b/indent/elixir.vim @@ -6,7 +6,7 @@ let b:did_indent = 1 setlocal indentexpr=elixir#indent(v:lnum) setlocal indentkeys+==after,=catch,=do,=else,=end,=rescue, -setlocal indentkeys+=*,=->,=\|>,=<>,0},0],0) +setlocal indentkeys+=*,=->,=\|>,=<>,0},0],0),> " TODO: @jbodah 2017-02-27: all operators should cause reindent when typed diff --git a/spec/indent/embedded_views_spec.rb b/spec/indent/embedded_views_spec.rb new file mode 100644 index 00000000..f60f1021 --- /dev/null +++ b/spec/indent/embedded_views_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Indenting embedded views' do + i <<~EOF + def render(assigns) do + ~L""" +
+ Some content +
+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~H""" +
+
+ This is immediately nested +
+ + There's a self-closing tag +
+
+
+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" +
+ Some content +
+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" +
+ Some content +
+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" +
+

Some paragraph

+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" +
+ it +
+ keeps +
+ nesting +
+
+
+ """ + end + EOF + + i <<~EOF + def render(assgins) do + ~L""" +
+ <%= for i <- iter do %> +
<%= i %>
+ <% end %> +
+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" + <%= live_component @socket, + Component, + id: "<%= @id %>", + user: @user do + %> + +
+
+

Some Header

+
+
+

Some Section

+

+ I'm some text +

+
+
+ + <% end %> + """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" + <%= render_component, + @socket, + Component do %> + +

Multi-line opening eex tag that takes a block

+ <% end %> + """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" +
+ <%= render_component, + @socket, + Component %> +
+ + <%= render_component, + @socket, + Component %> +

Multi-line single eex tag

+ """ + end + EOF + + i <<~EOF + def render(assigns) do + ~H""" + "bar" + } + }} + /> + """ + end + EOF + + i <<~EOF + def render(assigns) do + ~L""" + <%= live_component @socket, + Component, + id: "<%= @id %>", + team: @team do + %> + +
+
+
+ A deeply nested tree +
+ with trailing whitespace + +
+
+
+
+ +
+ + <%= for i <- iter do %> +
<%= i %>
+ <% end %> + +
+ +
    +
  • + {{ item }} +
  • +
+ +
+ Hi

hi

+ I'm ok, ok? +
+ hi there! +
+
+
+

hi

+
+
+
+
+ + + + +
content
+
+ +
+ +
hi
+ +
+
+ content +
+
+
+ content in new div after a self-closing div +
+
+ +

+ <%= @solo.eex_tag %> + + content + +

+ + <% end %> + """ + end + EOF +end