Skip to content

Commit d7ea2fa

Browse files
committed
Handle cursor completion inside fn/rescue/catch/else/after
1 parent fb03563 commit d7ea2fa

File tree

7 files changed

+80
-54
lines changed

7 files changed

+80
-54
lines changed

lib/eex/lib/eex/compiler.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ defmodule EEx.Compiler do
7171
{:ok, expr, new_line, new_column, rest} ->
7272
{key, expr} =
7373
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
74-
{:ok, _line, _column, _warnings, tokens} ->
74+
{:ok, _line, _column, _warnings, rev_tokens, []} ->
7575
# We ignore warnings because the code will be tokenized
7676
# again later with the right line+column info
77-
token_key(tokens, expr)
77+
token_key(rev_tokens, expr)
7878

7979
{:error, _, _, _, _} ->
8080
{:expr, expr}
@@ -164,8 +164,8 @@ defmodule EEx.Compiler do
164164
end
165165

166166
# Receives tokens and check if it is a start, middle or an end token.
167-
defp token_key(tokens, expr) do
168-
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
167+
defp token_key(rev_tokens, expr) do
168+
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
169169
{[{:end, _} | _], [{:do, _} | _]} ->
170170
{:middle_expr, expr}
171171

lib/elixir/lib/code/fragment.ex

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,10 @@ defmodule Code.Fragment do
481481

482482
defp operator(rest, count, acc, _call_op?) do
483483
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
484-
{:ok, _, _, _, [{:atom, _, _}]} ->
484+
{:ok, _, _, _, [{:atom, _, _}], []} ->
485485
{{:unquoted_atom, tl(acc)}, count}
486486

487-
{:ok, _, _, _, [{_, _, op}]} ->
487+
{:ok, _, _, _, [{_, _, op}], []} ->
488488
{rest, dot_count} = strip_spaces(rest, count)
489489

490490
cond do
@@ -1088,9 +1088,41 @@ defmodule Code.Fragment do
10881088
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
10891089
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
10901090
def container_cursor_to_quoted(fragment, opts \\ []) do
1091-
opts =
1092-
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
1091+
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
1092+
opts = [cursor_completion: true, emit_warnings: false] ++ opts
1093+
1094+
file = Keyword.get(opts, :file, "nofile")
1095+
line = Keyword.get(opts, :line, 1)
1096+
column = Keyword.get(opts, :column, 1)
1097+
1098+
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
1099+
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
1100+
tokens = :lists.reverse(rev_tokens, rev_terminators)
1101+
1102+
case :elixir.tokens_to_quoted(tokens, file, opts) do
1103+
{:ok, ast} ->
1104+
{:ok, ast}
1105+
1106+
{:error, error} ->
1107+
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
1108+
tokens =
1109+
:lists.reverse(
1110+
rev_tokens,
1111+
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
1112+
Enum.map(rev_terminators, fn tuple ->
1113+
{line, column, info} = elem(tuple, 1)
1114+
put_elem(tuple, 1, {line, column + 5, info})
1115+
end)
1116+
)
1117+
1118+
case :elixir.tokens_to_quoted(tokens, file, opts) do
1119+
{:ok, ast} -> {:ok, ast}
1120+
{:error, _} -> {:error, error}
1121+
end
1122+
end
10931123

1094-
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
1124+
{:error, info, _rest, _warnings, _so_far} ->
1125+
{:error, :elixir.format_token_error(info)}
1126+
end
10951127
end
10961128
end

lib/elixir/src/elixir.erl

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
-export([
88
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
99
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
10-
eval_quoted/4, eval_local_handler/2, eval_external_handler/3
10+
eval_quoted/4, eval_local_handler/2, eval_external_handler/3,
11+
format_token_error/1
1112
]).
1213
-include("elixir.hrl").
1314
-define(system, 'Elixir.System').
@@ -459,18 +460,21 @@ quoted_to_erl(Quoted, ErlS, ExS, Env) ->
459460

460461
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
461462
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, Opts) of
462-
{ok, _Line, _Column, [], Tokens} ->
463-
{ok, Tokens};
464-
{ok, _Line, _Column, Warnings, Tokens} ->
463+
{ok, _Line, _Column, [], Tokens, Terminators} ->
464+
{ok, lists:reverse(Tokens, Terminators)};
465+
{ok, _Line, _Column, Warnings, Tokens, Terminators} ->
465466
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
466467
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)],
467-
{ok, Tokens};
468-
{error, {Location, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _Warnings, _SoFar} ->
469-
{error, {Location, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
470-
{error, {Location, Error, Token}, _Rest, _Warnings, _SoFar} ->
471-
{error, {Location, to_binary(Error), to_binary(Token)}}
468+
{ok, lists:reverse(Tokens, Terminators)};
469+
{error, Info, _Rest, _Warnings, _SoFar} ->
470+
{error, format_token_error(Info)}
472471
end.
473472

473+
format_token_error({Location, {ErrorPrefix, ErrorSuffix}, Token}) ->
474+
{Location, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)};
475+
format_token_error({Location, Error, Token}) ->
476+
{Location, to_binary(Error), to_binary(Token)}.
477+
474478
tokens_to_quoted(Tokens, WarningFile, Opts) ->
475479
handle_parsing_opts(WarningFile, Opts),
476480

lib/elixir/src/elixir_interpolation.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
6262
extract(NewRest, [], Output2, EndLine, EndColumn + 1, NewScope, true, Last);
6363
{error, Reason, _, _, _} ->
6464
{error, Reason};
65-
{ok, EndLine, EndColumn, Warnings, Tokens} when Scope#elixir_tokenizer.cursor_completion /= false ->
65+
{ok, EndLine, EndColumn, Warnings, Tokens, Terminators} when Scope#elixir_tokenizer.cursor_completion /= false ->
6666
NewScope = Scope#elixir_tokenizer{warnings=Warnings, cursor_completion=noprune},
67-
Output2 = build_interpol(Line, Column, EndLine, EndColumn, Tokens, Output1),
67+
Output2 = build_interpol(Line, Column, EndLine, EndColumn, lists:reverse(Tokens, Terminators), Output1),
6868
extract([], [], Output2, EndLine, EndColumn, NewScope, true, Last);
69-
{ok, _, _, _, _} ->
69+
{ok, _, _, _, _, _} ->
7070
{error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}}
7171
end;
7272

lib/elixir/src/elixir_tokenizer.erl

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ tokenize(String, Line, Opts) ->
138138
tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope, Tokens) when Cursor /= false ->
139139
#elixir_tokenizer{ascii_identifiers_only=Ascii, terminators=Terminators, warnings=Warnings} = Scope,
140140

141-
{CursorColumn, CursorTerminators, CursorTokens} =
141+
{CursorColumn, CursorTerminators, AccTokens} =
142142
add_cursor(Line, Column, Cursor, Terminators, Tokens),
143143

144144
AllWarnings = maybe_unicode_lint_warnings(Ascii, Tokens, Warnings),
145-
AccTokens = cursor_complete(Line, CursorColumn, CursorTerminators, CursorTokens),
146-
{ok, Line, Column, AllWarnings, AccTokens};
145+
{AccTerminators, _AccColumn} = cursor_complete(Line, CursorColumn, CursorTerminators),
146+
{ok, Line, CursorColumn, AllWarnings, AccTokens, AccTerminators};
147147

148148
tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) ->
149149
End = terminator(Start),
@@ -163,7 +163,7 @@ tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLi
163163
tokenize([], Line, Column, #elixir_tokenizer{} = Scope, Tokens) ->
164164
#elixir_tokenizer{ascii_identifiers_only=Ascii, warnings=Warnings} = Scope,
165165
AllWarnings = maybe_unicode_lint_warnings(Ascii, Tokens, Warnings),
166-
{ok, Line, Column, AllWarnings, lists:reverse(Tokens)};
166+
{ok, Line, Column, AllWarnings, Tokens, []};
167167

168168
% VC merge conflict
169169

@@ -1728,19 +1728,15 @@ error(Reason, Rest, #elixir_tokenizer{warnings=Warnings}, Tokens) ->
17281728

17291729
%% Cursor handling
17301730

1731-
cursor_complete(Line, Column, Terminators, Tokens) ->
1732-
{AccTokens, _} =
1733-
lists:foldl(
1734-
fun({Start, _, _}, {NewTokens, NewColumn}) ->
1735-
End = terminator(Start),
1736-
AccTokens = [{End, {Line, NewColumn, nil}} | NewTokens],
1737-
AccColumn = NewColumn + length(erlang:atom_to_list(End)),
1738-
{AccTokens, AccColumn}
1739-
end,
1740-
{Tokens, Column},
1741-
Terminators
1742-
),
1743-
lists:reverse(AccTokens).
1731+
cursor_complete(Line, Column, Terminators) ->
1732+
lists:mapfoldl(
1733+
fun({Start, _, _}, AccColumn) ->
1734+
End = terminator(Start),
1735+
{{End, {Line, AccColumn, nil}}, AccColumn + length(erlang:atom_to_list(End))}
1736+
end,
1737+
Column,
1738+
Terminators
1739+
).
17441740

17451741
add_cursor(_Line, Column, noprune, Terminators, Tokens) ->
17461742
{Column, Terminators, Tokens};
@@ -1783,8 +1779,6 @@ prune_tokens([{'{', _} | Tokens], ['}' | Opener], Terminators) ->
17831779
prune_tokens([{'<<', _} | Tokens], ['>>' | Opener], Terminators) ->
17841780
prune_tokens(Tokens, Opener, Terminators);
17851781
%%% Handle anonymous functions
1786-
prune_tokens(Tokens, [], [{'fn', _, _} | Terminators]) ->
1787-
prune_tokens(drop_including(Tokens, 'fn'), [], Terminators);
17881782
prune_tokens([{'(', _}, {capture_op, _, _} | Tokens], [], [{'(', _, _} | Terminators]) ->
17891783
prune_tokens(Tokens, [], Terminators);
17901784
%%% or it is time to stop...
@@ -1794,6 +1788,8 @@ prune_tokens([{'eol', _} | _] = Tokens, [], Terminators) ->
17941788
{Tokens, Terminators};
17951789
prune_tokens([{',', _} | _] = Tokens, [], Terminators) ->
17961790
{Tokens, Terminators};
1791+
prune_tokens([{'fn', _} | _] = Tokens, [], Terminators) ->
1792+
{Tokens, Terminators};
17971793
prune_tokens([{'do', _} | _] = Tokens, [], Terminators) ->
17981794
{Tokens, Terminators};
17991795
prune_tokens([{'(', _} | _] = Tokens, [], Terminators) ->
@@ -1827,7 +1823,3 @@ prune_tokens([_ | Tokens], Opener, Terminators) ->
18271823
prune_tokens(Tokens, Opener, Terminators);
18281824
prune_tokens([], [], Terminators) ->
18291825
{[], Terminators}.
1830-
1831-
drop_including([{Token, _} | Tokens], Token) -> Tokens;
1832-
drop_including([_ | Tokens], Token) -> drop_including(Tokens, Token);
1833-
drop_including([], _Token) -> [].

lib/elixir/test/elixir/code_fragment_test.exs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,9 +1158,7 @@ defmodule CodeFragmentTest do
11581158
assert cc2q!("foo(bar do baz ") == s2q!("foo(bar do baz(__cursor__()) end)")
11591159
assert cc2q!("foo(bar do baz(") == s2q!("foo(bar do baz(__cursor__()) end)")
11601160
assert cc2q!("foo(bar do baz bat,") == s2q!("foo(bar do baz(bat, __cursor__()) end)")
1161-
1162-
assert {:error, {_, "syntax error before: ", "'end'"}} =
1163-
CF.container_cursor_to_quoted("foo(bar do baz, bat")
1161+
assert cc2q!("foo(bar do baz, bat") == s2q!("foo(bar do baz, __cursor__() -> nil end)")
11641162
end
11651163

11661164
test "keyword lists" do
@@ -1232,11 +1230,11 @@ defmodule CodeFragmentTest do
12321230
end
12331231

12341232
test "removes anonymous functions" do
1235-
assert cc2q!("(fn") == s2q!("(__cursor__())")
1236-
assert cc2q!("(fn x") == s2q!("(__cursor__())")
1237-
assert cc2q!("(fn x ->") == s2q!("(__cursor__())")
1238-
assert cc2q!("(fn x -> x") == s2q!("(__cursor__())")
1239-
assert cc2q!("(fn x, y -> x + y") == s2q!("(__cursor__())")
1233+
assert cc2q!("(fn") == s2q!("(fn __cursor__() -> nil end)")
1234+
assert cc2q!("(fn x") == s2q!("(fn __cursor__() -> nil end)")
1235+
assert cc2q!("(fn x ->") == s2q!("(fn x -> __cursor__() end)")
1236+
assert cc2q!("(fn x -> x") == s2q!("(fn x -> __cursor__() end)")
1237+
assert cc2q!("(fn x, y -> x + y") == s2q!("(fn x, y -> x + __cursor__() end)")
12401238
assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())")
12411239
end
12421240

lib/elixir/test/erlang/tokenizer_test.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ tokenize(String) ->
55
tokenize(String, []).
66

77
tokenize(String, Opts) ->
8-
{ok, _Line, _Column, _Warnings, Result} = elixir_tokenizer:tokenize(String, 1, Opts),
9-
Result.
8+
{ok, _Line, _Column, _Warnings, Result, []} = elixir_tokenizer:tokenize(String, 1, Opts),
9+
lists:reverse(Result).
1010

1111
tokenize_error(String) ->
1212
{error, Error, _, _, _} = elixir_tokenizer:tokenize(String, 1, []),

0 commit comments

Comments
 (0)