Skip to content

Commit faf81cd

Browse files
committed
Improve docs for fragments, keep previous lines in blocks
1 parent 684130d commit faf81cd

File tree

3 files changed

+72
-26
lines changed

3 files changed

+72
-26
lines changed

lib/elixir/lib/code/fragment.ex

+64-26
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ defmodule Code.Fragment do
1717
1818
This function receives a string with an Elixir code fragment,
1919
representing a cursor position, and based on the string, it
20-
provides contextual information about said position. The
21-
return of this function can then be used to provide tips,
20+
provides contextual information about the latest token.
21+
The return of this function can then be used to provide tips,
2222
suggestions, and autocompletion functionality.
2323
24-
This function provides a best-effort detection and may not be
25-
accurate under all circumstances. See the "Limitations"
26-
section below.
24+
This function performs its analyses on tokens. This means
25+
it does not understand how constructs are nested within each
26+
other. See the "Limitations" section below.
2727
2828
Consider adding a catch-all clause when handling the return
2929
type of this function as new cursor information may be added
@@ -110,11 +110,28 @@ defmodule Code.Fragment do
110110
* `{:unquoted_atom, charlist}` - the context is an unquoted atom. This
111111
can be any atom or an atom representing a module
112112
113+
We recommend looking at the test suite of this function for a complete list
114+
of examples and their return values.
115+
113116
## Limitations
114117
115-
The current algorithm only considers the last line of the input. This means
116-
it will also show suggestions inside strings, heredocs, etc, which is
117-
intentional as it helps with doctests, references, and more.
118+
The analysis is based on the current token, by analysing the last line of
119+
the input. For example, this code:
120+
121+
iex> Code.Fragment.cursor_context("%URI{")
122+
:expr
123+
124+
returns `:expr`, which suggests any variable, local function or alias
125+
could be used. However, given we are inside a struct, the best suggestion
126+
would be a struct field. In such cases, you can use
127+
`container_cursor_to_quoted`, which will return the container of the AST
128+
the cursor is currently within. You can then analyse this AST to provide
129+
completion of field names.
130+
131+
As a consequence of its token-based implementation, this function considers
132+
only the last line of the input. This means it will show suggestions inside
133+
strings, heredocs, etc, which is intentional as it helps with doctests,
134+
references, and more.
118135
"""
119136
@doc since: "1.13.0"
120137
@spec cursor_context(List.Chars.t(), keyword()) ::
@@ -480,7 +497,8 @@ defmodule Code.Fragment do
480497
This function receives a string with an Elixir code fragment
481498
and a `position`. It returns a map containing the beginning
482499
and ending of the identifier alongside its context, or `:none`
483-
if there is nothing with a known context.
500+
if there is nothing with a known context. This is useful to
501+
provide mouse-over and highlight functionality in editors.
484502
485503
The difference between `cursor_context/2` and `surround_context/3`
486504
is that the former assumes the expression in the code fragment
@@ -509,10 +527,10 @@ defmodule Code.Fragment do
509527
The returned map contains the column the expression starts and the
510528
first column after the expression ends.
511529
512-
Similar to `cursor_context/2`, this function also provides a best-effort
513-
detection and may not be accurate under all circumstances. See the
514-
"Return values" and "Limitations" section under `cursor_context/2` for
515-
more information.
530+
Similar to `cursor_context/2`, this function is also token-based
531+
and may not be accurate under all circumstances. See the
532+
"Return values" and "Limitations" section under `cursor_context/2`
533+
for more information.
516534
517535
## Examples
518536
@@ -521,8 +539,8 @@ defmodule Code.Fragment do
521539
522540
## Differences to `cursor_context/2`
523541
524-
Because `surround_context/3` deals with complete code, it has some
525-
difference to `cursor_context/2`:
542+
Because `surround_context/3` attempts to capture complex expressions,
543+
it has some differences to `cursor_context/2`:
526544
527545
* `dot_call`/`dot_arity` and `operator_call`/`operator_arity`
528546
are collapsed into `dot` and `operator` contexts respectively
@@ -542,6 +560,8 @@ defmodule Code.Fragment do
542560
543561
* This function never returns `:expr`
544562
563+
We recommend looking at the test suite of this function for a complete list
564+
of examples and their return values.
545565
"""
546566
@doc since: "1.13.0"
547567
@spec surround_context(List.Chars.t(), position(), keyword()) ::
@@ -937,17 +957,15 @@ defmodule Code.Fragment do
937957

938958
@doc """
939959
Receives a string and returns a quoted expression
940-
with a cursor at the nearest argument position.
960+
with the cursor AST position within its parent expression.
941961
942962
This function receives a string with an Elixir code fragment,
943963
representing a cursor position, and converts such string to
944-
AST with the inclusion of special `__cursor__()` node based
945-
on the position of the cursor with a container.
964+
AST with the inclusion of special `__cursor__()` node representing
965+
the cursor position within its parent.
946966
947-
A container is any Elixir expression starting with `(`,
948-
`{`, and `[`. This includes function calls, tuples, lists,
949-
maps, and so on. For example, take this code, which would
950-
be given as input:
967+
The parent node is any function call, tuple, list, map, and so on.
968+
For example, take this code, which would be given as input:
951969
952970
max(some_value,
953971
@@ -956,9 +974,9 @@ defmodule Code.Fragment do
956974
max(some_value, __cursor__())
957975
958976
In other words, this function is capable of closing any open
959-
brackets and insert the cursor position. Any content at the
960-
cursor position that is after a comma or an opening bracket
961-
is discarded. For example, if this is given as input:
977+
brackets and insert the cursor position. Other content at the
978+
cursor position which is not a parent is discarded.
979+
For example, if this is given as input:
962980
963981
max(some_value, another_val
964982
@@ -1004,6 +1022,8 @@ defmodule Code.Fragment do
10041022
10051023
if(some_value, do: __cursor__())
10061024
1025+
For multi-line blocks, all previous lines are preserved.
1026+
10071027
The AST returned by this function is not safe to evaluate but
10081028
it can be analyzed and expanded.
10091029
@@ -1019,7 +1039,25 @@ defmodule Code.Fragment do
10191039
iex> Code.Fragment.container_cursor_to_quoted("[some, value")
10201040
{:ok, [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}
10211041
1022-
For binaries, the `::` is exclusively kept as an operator:
1042+
If an expression is complete, then the whole expression is discarded
1043+
and only the parent is returned:
1044+
1045+
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var)")
1046+
{:ok, {:if, [line: 1], [{:__cursor__, [line: 1], []}]}}
1047+
1048+
this means complete expressions themselves return only the cursor:
1049+
1050+
iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var))")
1051+
{:ok, {:__cursor__, [line: 1], []}}
1052+
iex> Code.Fragment.container_cursor_to_quoted("alias Foo.Bar")
1053+
{:ok, {:__cursor__, [line: 1], []}}
1054+
1055+
Operators are never considered containers:
1056+
1057+
iex> Code.Fragment.container_cursor_to_quoted("if(foo +")
1058+
{:ok, {:if, [line: 1], [{:__cursor__, [line: 1], []}]}}
1059+
1060+
with the exception of `::` inside binaries and `|` inside maps:
10231061
10241062
iex> Code.Fragment.container_cursor_to_quoted("<<some::integer")
10251063
{:ok, {:<<>>, [line: 1], [{:"::", [line: 1], [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}]}}

lib/elixir/src/elixir_tokenizer.erl

+4
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,10 @@ prune_tokens(Tokens, [], [{'fn', _, _} | Terminators]) ->
17601760
prune_tokens([{'(', _}, {capture_op, _, _} | Tokens], [], [{'(', _, _} | Terminators]) ->
17611761
prune_tokens(Tokens, [], Terminators);
17621762
%%% or it is time to stop...
1763+
prune_tokens([{';', _} | _] = Tokens, [], Terminators) ->
1764+
{Tokens, Terminators};
1765+
prune_tokens([{'eol', _} | _] = Tokens, [], Terminators) ->
1766+
{Tokens, Terminators};
17631767
prune_tokens([{',', _} | _] = Tokens, [], Terminators) ->
17641768
{Tokens, Terminators};
17651769
prune_tokens([{'do', _} | _] = Tokens, [], Terminators) ->

lib/elixir/test/elixir/code_fragment_test.exs

+4
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,10 @@ defmodule CodeFragmentTest do
10971097
end
10981098

10991099
test "do-end blocks" do
1100+
assert cc2q("foo do baz") == s2q("foo do __cursor__() end")
1101+
assert cc2q("foo do bar; baz") == s2q("foo do bar; __cursor__() end")
1102+
assert cc2q("foo do bar\nbaz") == s2q("foo do bar\n__cursor__() end")
1103+
11001104
assert cc2q("foo(bar do baz") == s2q("foo(bar do __cursor__() end)")
11011105
assert cc2q("foo(bar do baz ") == s2q("foo(bar do baz(__cursor__()) end)")
11021106
assert cc2q("foo(bar do baz(") == s2q("foo(bar do baz(__cursor__()) end)")

0 commit comments

Comments
 (0)