Skip to content

Commit 23bb088

Browse files
authored
Add Code.Fragment.container_cursor_to_quoted/2 (#11315)
1 parent 8203cf1 commit 23bb088

File tree

10 files changed

+505
-182
lines changed

10 files changed

+505
-182
lines changed

lib/eex/lib/eex/tokenizer.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ defmodule EEx.Tokenizer 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, warnings, tokens} ->
75-
Enum.each(warnings, fn {line, file, msg} ->
74+
{:ok, _line, _column, warnings, tokens} ->
75+
Enum.each(Enum.reverse(warnings), fn {line, file, msg} ->
7676
:elixir_errors.erl_warn(line, file, msg)
7777
end)
7878

7979
token_key(tokens, expr)
8080

81-
{:error, _, _, _} ->
81+
{:error, _, _, _, _} ->
8282
{:expr, expr}
8383
end
8484

lib/elixir/lib/code/fragment.ex

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ defmodule Code.Fragment do
104104
105105
The current algorithm only considers the last line of the input. This means
106106
it will also show suggestions inside strings, heredocs, etc, which is
107-
intentional as it helps with doctests, references, and more. Other functions
108-
may be added in the future that consider the tree-structure of the code.
107+
intentional as it helps with doctests, references, and more.
109108
"""
110109
@doc since: "1.13.0"
111110
@spec cursor_context(List.Chars.t(), keyword()) ::
@@ -405,10 +404,10 @@ defmodule Code.Fragment do
405404

406405
defp operator(rest, count, acc, _call_op?) do
407406
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
408-
{:ok, _, [{:atom, _, _}]} ->
407+
{:ok, _, _, _, [{:atom, _, _}]} ->
409408
{{:unquoted_atom, tl(acc)}, count}
410409

411-
{:ok, _, [{_, _, op}]} ->
410+
{:ok, _, _, _, [{_, _, op}]} ->
412411
{rest, dot_count} = strip_spaces(rest, count)
413412

414413
cond do
@@ -720,4 +719,116 @@ defmodule Code.Fragment do
720719

721720
defp enum_reverse_at([h | t], n, acc) when n > 0, do: enum_reverse_at(t, n - 1, [h | acc])
722721
defp enum_reverse_at(rest, _, acc), do: {acc, rest}
722+
723+
@doc """
724+
Receives a code fragment and returns a quoted expression
725+
with a cursor at the nearest argument position.
726+
727+
A container is any Elixir expression starting with `(`,
728+
`{`, and `[`. This includes function calls, tuples, lists,
729+
maps, and so on. For example, take this code, which would
730+
be given as input:
731+
732+
max(some_value,
733+
734+
This function will return the AST equivalent to:
735+
736+
max(some_value, __cursor__())
737+
738+
In other words, this function is capable of closing any open
739+
brackets and insert the cursor position. Any content at the
740+
cursor position that is after a comma or an opening bracket
741+
is discarded. For example, if this is given as input:
742+
743+
max(some_value, another_val
744+
745+
It will return the same AST:
746+
747+
max(some_value, __cursor__())
748+
749+
Similarly, if only this is given:
750+
751+
max(some_va
752+
753+
Then it returns:
754+
755+
max(__cursor__())
756+
757+
Calls without parenthesis are also supported, as we assume the
758+
brackets are implicit.
759+
760+
Operators and anonymous functions are not containers, and therefore
761+
will be discarded. The following will all return the same AST:
762+
763+
max(some_value,
764+
max(some_value, fn x -> x end
765+
max(some_value, 1 + another_val
766+
max(some_value, 1 |> some_fun() |> another_fun
767+
768+
On the other hand, tuples, lists, maps, etc all retain the
769+
cursor position:
770+
771+
max(some_value, [1, 2,
772+
773+
Returns the following AST:
774+
775+
max(some_value, [1, 2, __cursor__()])
776+
777+
Keyword lists (and do-end blocks) are also retained. The following:
778+
779+
if(some_value, do:
780+
if(some_value, do: :token
781+
if(some_value, do: 1 + val
782+
783+
all return:
784+
785+
if(some_value, do: __cursor__())
786+
787+
The AST returned by this function is not safe to evaluate but
788+
it can be analyzed and expanded.
789+
790+
## Examples
791+
792+
iex> Code.Fragment.container_cursor_to_quoted("max(some_value, ")
793+
{:ok, {:max, [line: 1], [{:some_value, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
794+
795+
## Options
796+
797+
* `:file` - the filename to be reported in case of parsing errors.
798+
Defaults to `"nofile"`.
799+
800+
* `:line` - the starting line of the string being parsed.
801+
Defaults to 1.
802+
803+
* `:column` - the starting column of the string being parsed.
804+
Defaults to 1.
805+
806+
* `:columns` - when `true`, attach a `:column` key to the quoted
807+
metadata. Defaults to `false`.
808+
809+
"""
810+
@doc since: "1.13.0"
811+
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
812+
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
813+
def container_cursor_to_quoted(fragment, opts \\ []) do
814+
file = Keyword.get(opts, :file, "nofile")
815+
line = Keyword.get(opts, :line, 1)
816+
column = Keyword.get(opts, :column, 1)
817+
columns = Keyword.get(opts, :columns, false)
818+
fragment = to_charlist(fragment)
819+
tokenizer_opts = [file: file, cursor_completion: true, columns: columns]
820+
821+
case :elixir_tokenizer.tokenize(fragment, line, column, tokenizer_opts) do
822+
{:ok, _, _, _warnings, tokens} ->
823+
:elixir.tokens_to_quoted(tokens, file, columns: columns)
824+
825+
{:error, {line, column, {prefix, suffix}, token}, _rest, _warnings, _so_far} ->
826+
location = [line: line, column: column]
827+
{:error, {location, {to_string(prefix), to_string(suffix)}, to_string(token)}}
828+
829+
{:error, {line, column, error, token}, _rest, _warnings, _so_far} ->
830+
location = [line: line, column: column]
831+
{:error, {location, to_string(error), to_string(token)}}
832+
end
833+
end
723834
end

lib/elixir/src/elixir.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,13 +348,13 @@ quoted_to_erl(Quoted, Env, Scope) ->
348348

349349
string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(StartLine), is_binary(File) ->
350350
case elixir_tokenizer:tokenize(String, StartLine, StartColumn, [{file, File} | Opts]) of
351-
{ok, Warnings, Tokens} ->
352-
[elixir_errors:erl_warn(L, F, M) || {L, F, M} <- Warnings],
351+
{ok, _Line, _Column, Warnings, Tokens} ->
352+
[elixir_errors:erl_warn(L, F, M) || {L, F, M} <- lists:reverse(Warnings)],
353353
{ok, Tokens};
354-
{error, {Line, Column, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _SoFar} ->
354+
{error, {Line, Column, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _Warnings, _SoFar} ->
355355
Location = [{line, Line}, {column, Column}],
356356
{error, {Location, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
357-
{error, {Line, Column, Error, Token}, _Rest, _SoFar} ->
357+
{error, {Line, Column, Error, Token}, _Rest, _Warnings, _SoFar} ->
358358
Location = [{line, Line}, {column, Column}],
359359
{error, {Location, to_binary(Error), to_binary(Token)}}
360360
end.

lib/elixir/src/elixir.hrl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
file=(<<"nofile">>),
2929
terminators=[],
3030
unescape=true,
31+
cursor_completion=false,
3132
existing_atoms_only=false,
3233
static_atoms_encoder=nil,
3334
preserve_comments=nil,

lib/elixir/src/elixir_interpolation.erl

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,72 +9,78 @@ unescape_tokens/1, unescape_map/1]).
99

1010
%% Extract string interpolations
1111

12-
extract(Line, Column, Raw, Interpol, String, Last) ->
13-
%% Ignore whatever is in the scope and enable terminator checking.
14-
Scope = Raw#elixir_tokenizer{terminators=[]},
15-
extract(Line, Column, Scope, Interpol, String, [], [], Last).
12+
extract(Line, Column, Scope, Interpol, String, Last) ->
13+
extract(String, [], [], Line, Column, Scope, Interpol, Last).
1614

1715
%% Terminators
1816

19-
extract(Line, Column, _Scope, _Interpol, [], _Buffer, _Output, Last) ->
17+
extract([], _Buffer, _Output, Line, Column, #elixir_tokenizer{cursor_completion=false}, _Interpol, Last) ->
2018
{error, {string, Line, Column, io_lib:format("missing terminator: ~ts", [[Last]]), []}};
2119

22-
extract(Line, Column, _Scope, _Interpol, [Last | Rest], Buffer, Output, Last) ->
23-
finish_extraction(Line, Column + 1, Buffer, Output, Rest);
20+
extract([], Buffer, Output, Line, Column, Scope, _Interpol, _Last) ->
21+
finish_extraction([], Buffer, Output, Line, Column, Scope);
22+
23+
extract([Last | Rest], Buffer, Output, Line, Column, Scope, _Interpol, Last) ->
24+
finish_extraction(Rest, Buffer, Output, Line, Column + 1, Scope);
2425

2526
%% Going through the string
2627

27-
extract(Line, _Column, Scope, Interpol, [$\\, $\r, $\n | Rest], Buffer, Output, Last) ->
28-
extract_nl(Line, Scope, Interpol, Rest, [$\n, $\r, $\\ | Buffer], Output, Last);
28+
extract([$\\, $\r, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
29+
extract_nl(Rest, [$\n, $\r, $\\ | Buffer], Output, Line, Scope, Interpol, Last);
2930

30-
extract(Line, _Column, Scope, Interpol, [$\\, $\n | Rest], Buffer, Output, Last) ->
31-
extract_nl(Line, Scope, Interpol, Rest, [$\n, $\\ | Buffer], Output, Last);
31+
extract([$\\, $\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
32+
extract_nl(Rest, [$\n, $\\ | Buffer], Output, Line, Scope, Interpol, Last);
3233

33-
extract(Line, _Column, Scope, Interpol, [$\n | Rest], Buffer, Output, Last) ->
34-
extract_nl(Line, Scope, Interpol, Rest, [$\n | Buffer], Output, Last);
34+
extract([$\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) ->
35+
extract_nl(Rest, [$\n | Buffer], Output, Line, Scope, Interpol, Last);
3536

36-
extract(Line, Column, Scope, Interpol, [$\\, Last | Rest], Buffer, Output, Last) ->
37-
extract(Line, Column+2, Scope, Interpol, Rest, [Last | Buffer], Output, Last);
37+
extract([$\\, Last | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) ->
38+
extract(Rest, [Last | Buffer], Output, Line, Column+2, Scope, Interpol, Last);
3839

39-
extract(Line, Column, Scope, true, [$\\, $#, ${ | Rest], Buffer, Output, Last) ->
40-
extract(Line, Column+1, Scope, true, Rest, [${, $#, $\\ | Buffer], Output, Last);
40+
extract([$\\, $#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
41+
extract(Rest, [${, $#, $\\ | Buffer], Output, Line, Column+1, Scope, true, Last);
4142

42-
extract(Line, Column, Scope, true, [$#, ${ | Rest], Buffer, Output, Last) ->
43-
Output1 = build_string(Line, Buffer, Output),
44-
case elixir_tokenizer:tokenize(Rest, Line, Column + 2, Scope) of
45-
{error, {EndLine, EndColumn, _, "}"}, [$} | NewRest], Tokens} ->
46-
Output2 = build_interpol(Line, Column, EndLine, EndColumn, Tokens, Output1),
47-
extract(EndLine, EndColumn + 1, Scope, true, NewRest, [], Output2, Last);
48-
{error, Reason, _, _} ->
43+
extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
44+
Output1 = build_string(Buffer, Output),
45+
case elixir_tokenizer:tokenize(Rest, Line, Column + 2, Scope#elixir_tokenizer{terminators=[]}) of
46+
{error, {EndLine, EndColumn, _, "}"}, [$} | NewRest], Warnings, Tokens} ->
47+
NewScope = Scope#elixir_tokenizer{warnings=Warnings},
48+
Output2 = build_interpol(Line, Column, EndLine, EndColumn, lists:reverse(Tokens), Output1),
49+
extract(NewRest, [], Output2, EndLine, EndColumn + 1, NewScope, true, Last);
50+
{error, Reason, _, _, _} ->
4951
{error, Reason};
50-
{ok, _, _} ->
52+
{ok, EndLine, EndColumn, Warnings, Tokens} when Scope#elixir_tokenizer.cursor_completion /= false ->
53+
NewScope = Scope#elixir_tokenizer{warnings=Warnings, cursor_completion=terminators},
54+
Output2 = build_interpol(Line, Column, EndLine, EndColumn, Tokens, Output1),
55+
extract([], [], Output2, EndLine, EndColumn, NewScope, true, Last);
56+
{ok, _, _, _, _} ->
5157
{error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}}
5258
end;
5359

54-
extract(Line, Column, Scope, Interpol, [$\\, Char | Rest], Buffer, Output, Last) ->
55-
extract(Line, Column + 2, Scope, Interpol, Rest, [Char, $\\ | Buffer], Output, Last);
60+
extract([$\\, Char | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) ->
61+
extract(Rest, [Char, $\\ | Buffer], Output, Line, Column + 2, Scope, Interpol, Last);
5662

5763
%% Catch all clause
5864

59-
extract(Line, Column, Scope, Interpol, [Char1, Char2 | Rest], Buffer, Output, Last)
65+
extract([Char1, Char2 | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last)
6066
when Char1 =< 255, Char2 =< 255 ->
61-
extract(Line, Column + 1, Scope, Interpol, [Char2 | Rest], [Char1 | Buffer], Output, Last);
67+
extract([Char2 | Rest], [Char1 | Buffer], Output, Line, Column + 1, Scope, Interpol, Last);
6268

63-
extract(Line, Column, Scope, Interpol, Rest, Buffer, Output, Last) ->
69+
extract(Rest, Buffer, Output, Line, Column, Scope, Interpol, Last) ->
6470
[Char | NewRest] = unicode_util:gc(Rest),
65-
extract(Line, Column + 1, Scope, Interpol, NewRest, [Char | Buffer], Output, Last).
71+
extract(NewRest, [Char | Buffer], Output, Line, Column + 1, Scope, Interpol, Last).
6672

6773
%% Handle newlines. Heredocs require special attention
6874

69-
extract_nl(Line, Scope, Interpol, Rest, Buffer, Output, [H,H,H] = Last) ->
75+
extract_nl(Rest, Buffer, Output, Line, Scope, Interpol, [H,H,H] = Last) ->
7076
case strip_horizontal_space(Rest, Buffer, 1) of
7177
{[H,H,H|NewRest], _NewBuffer, Column} ->
72-
finish_extraction(Line + 1, Column + 3, Buffer, Output, NewRest);
78+
finish_extraction(NewRest, Buffer, Output, Line + 1, Column + 3, Scope);
7379
{NewRest, NewBuffer, Column} ->
74-
extract(Line + 1, Column, Scope, Interpol, NewRest, NewBuffer, Output, Last)
80+
extract(NewRest, NewBuffer, Output, Line + 1, Column, Scope, Interpol, Last)
7581
end;
76-
extract_nl(Line, Scope, Interpol, Rest, Buffer, Output, Last) ->
77-
extract(Line + 1, 1, Scope, Interpol, Rest, Buffer, Output, Last).
82+
extract_nl(Rest, Buffer, Output, Line, Scope, Interpol, Last) ->
83+
extract(Rest, Buffer, Output, Line + 1, 1, Scope, Interpol, Last).
7884

7985
strip_horizontal_space([H | T], Buffer, Counter) when H =:= $\s; H =:= $\t ->
8086
strip_horizontal_space(T, [H | Buffer], Counter + 1);
@@ -243,16 +249,16 @@ unescape_map(E) -> E.
243249

244250
% Extract Helpers
245251

246-
finish_extraction(Line, Column, Buffer, Output, Remaining) ->
247-
Final = case build_string(Line, Buffer, Output) of
252+
finish_extraction(Remaining, Buffer, Output, Line, Column, Scope) ->
253+
Final = case build_string(Buffer, Output) of
248254
[] -> [[]];
249255
F -> F
250256
end,
251257

252-
{Line, Column, lists:reverse(Final), Remaining}.
258+
{Line, Column, lists:reverse(Final), Remaining, Scope}.
253259

254-
build_string(_Line, [], Output) -> Output;
255-
build_string(_Line, Buffer, Output) -> [lists:reverse(Buffer) | Output].
260+
build_string([], Output) -> Output;
261+
build_string(Buffer, Output) -> [lists:reverse(Buffer) | Output].
256262

257263
build_interpol(Line, Column, EndLine, EndColumn, Buffer, Output) ->
258-
[{{Line, Column, nil}, {EndLine, EndColumn, nil}, lists:reverse(Buffer)} | Output].
264+
[{{Line, Column, nil}, {EndLine, EndColumn, nil}, Buffer} | Output].

0 commit comments

Comments
 (0)