Skip to content

Commit 87bba10

Browse files
committed
Add support for :group doc metadata and use it in IEx.Autocomplete
1 parent 7fdfe77 commit 87bba10

File tree

3 files changed

+121
-50
lines changed

3 files changed

+121
-50
lines changed

lib/elixir/pages/getting-started/writing-documentation.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ end
6161

6262
## Documentation metadata
6363

64-
Elixir allows developers to attach arbitrary metadata to the documentation. This is done by passing a keyword list to the relevant attribute (such as `@moduledoc`, `@typedoc`, and `@doc`). A commonly used metadata is `:since`, which annotates in which version that particular module, function, type, or callback was added, as shown in the example above.
64+
Elixir allows developers to attach arbitrary metadata to the documentation. This is done by passing a keyword list to the relevant attribute (such as `@moduledoc`, `@typedoc`, and `@doc`).
65+
66+
Metadata can have any key. Documentation tools often use metadata to provide more data to readers and to enrich the user experience. The following keys already have a predefined meaning used by tooling:
67+
68+
### `:deprecated`
6569

6670
Another common metadata is `:deprecated`, which emits a warning in the documentation, explaining that its usage is discouraged:
6771

@@ -75,7 +79,28 @@ Note that the `:deprecated` key does not warn when a developer invokes the funct
7579
@deprecated "Use Foo.bar/2 instead"
7680
```
7781

78-
Metadata can have any key. Documentation tools often use metadata to provide more data to readers and to enrich the user experience.
82+
### `:group`
83+
84+
The group a function, callback or type belongs to. This is used in `iex` for autocompleting and also to automatically by [ExDoc](https://github.com/elixir-lang/ex_doc/) to group items in the sidebar:
85+
86+
```elixir
87+
@doc group: "Query"
88+
def all(query)
89+
90+
@doc group: "Schema"
91+
def insert(schema)
92+
```
93+
94+
### `:since`
95+
96+
It annotates in which version that particular module, function, type, or callback was added:
97+
98+
```elixir
99+
@doc since: "1.3.0"
100+
def world(name) do
101+
IO.puts("hello #{name}")
102+
end
103+
```
79104

80105
## Recommendations
81106

lib/iex/lib/iex/autocomplete.ex

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ defmodule IEx.Autocomplete do
1010
%{kind: :variable, name: "little"},
1111
%{kind: :variable, name: "native"},
1212
%{kind: :variable, name: "signed"},
13-
%{kind: :function, name: "size", arity: 1},
14-
%{kind: :function, name: "unit", arity: 1},
13+
%{kind: :export, name: "size", arity: 1},
14+
%{kind: :export, name: "unit", arity: 1},
1515
%{kind: :variable, name: "unsigned"},
1616
%{kind: :variable, name: "utf8"},
1717
%{kind: :variable, name: "utf16"},
@@ -162,7 +162,7 @@ defmodule IEx.Autocomplete do
162162
{:ok, mod} when is_atom(mod) ->
163163
mod
164164
|> fun.()
165-
|> match_module_funs(hint, false)
165+
|> match_exports(hint, false)
166166
|> format_expansion(hint)
167167

168168
_ ->
@@ -197,7 +197,7 @@ defmodule IEx.Autocomplete do
197197
end
198198

199199
defp expand_signatures([_ | _] = signatures, _shell) do
200-
yes("", Enum.sort_by(signatures, &String.length/1))
200+
yes(~c"", signatures |> Enum.map(&String.to_charlist/1) |> Enum.sort_by(&length/1))
201201
end
202202

203203
defp expand_signatures([], shell), do: expand_local_or_var("", shell)
@@ -251,14 +251,13 @@ defmodule IEx.Autocomplete do
251251
end
252252

253253
defp expand_dot_aliases(mod) do
254-
all = match_elixir_modules(mod, "") ++ match_module_funs(get_module_funs(mod), "", false)
254+
all = match_elixir_modules(mod, "") ++ get_and_match_module_defs(mod, "", false)
255255
format_expansion(all, "")
256256
end
257257

258258
defp expand_require(mod, hint, exact?) do
259259
mod
260-
|> get_module_funs()
261-
|> match_module_funs(hint, exact?)
260+
|> get_and_match_module_defs(hint, exact?)
262261
|> format_expansion(hint)
263262
end
264263

@@ -282,8 +281,8 @@ defmodule IEx.Autocomplete do
282281

283282
defp match_local(hint, exact?, shell) do
284283
imports = imports_from_env(shell) |> Enum.flat_map(&elem(&1, 1))
285-
module_funs = get_module_funs(Kernel.SpecialForms)
286-
match_module_funs(imports ++ module_funs, hint, exact?)
284+
module_funs = exports(Kernel.SpecialForms)
285+
match_exports(imports ++ module_funs, hint, exact?)
287286
end
288287

289288
defp match_var(hint, shell) do
@@ -520,7 +519,7 @@ defmodule IEx.Autocomplete do
520519

521520
defp format_expansion([uniq], hint) do
522521
case to_hint(uniq, hint) do
523-
"" -> yes("", to_entries(uniq))
522+
~c"" -> yes(~c"", [to_entry(uniq)])
524523
hint -> yes(hint, [])
525524
end
526525
end
@@ -531,14 +530,32 @@ defmodule IEx.Autocomplete do
531530
prefix = :binary.longest_common_prefix(binary)
532531

533532
if prefix in [0, length] do
534-
yes("", Enum.flat_map(entries, &to_entries/1))
533+
case Enum.group_by(entries, &Map.get(&1, :group, "Exports")) do
534+
groups when map_size(groups) == 1 ->
535+
yes(~c"", Enum.map(entries, &to_entry/1))
536+
537+
groups ->
538+
sections =
539+
groups
540+
|> Enum.map(fn {group, entries} ->
541+
%{
542+
title: to_charlist(group),
543+
options: [{:highlight_all}],
544+
elems: Enum.map(entries, &to_entry/1)
545+
}
546+
end)
547+
|> Enum.sort_by(&length(&1.elems))
548+
549+
yes(~c"", sections)
550+
end
535551
else
536-
yes(binary_part(first.name, prefix, length - prefix), [])
552+
hint = binary_part(first.name, prefix, length - prefix)
553+
yes(String.to_charlist(hint), [])
537554
end
538555
end
539556

540-
defp yes(hint, entries) do
541-
{:yes, String.to_charlist(hint), Enum.map(entries, &String.to_charlist/1)}
557+
defp yes(hint, entries) when is_list(hint) and is_list(entries) do
558+
{:yes, hint, entries}
542559
end
543560

544561
defp no do
@@ -591,40 +608,47 @@ defmodule IEx.Autocomplete do
591608
:ets.match(:ac_tab, {{:loaded, :"$1"}, :_})
592609
end
593610

594-
defp match_module_funs(funs, hint, exact?) do
611+
defp match_map_fields(map, hint) do
612+
for {key, value} when is_atom(key) <- Map.to_list(map),
613+
key = Atom.to_string(key),
614+
String.starts_with?(key, hint) do
615+
%{kind: :map_key, name: key, value_is_map: is_map(value)}
616+
end
617+
|> Enum.sort_by(& &1.name)
618+
end
619+
620+
defp match_exports(funs, hint, exact?) do
595621
for {fun, arity} <- funs,
596622
name = Atom.to_string(fun),
597623
if(exact?, do: name == hint, else: String.starts_with?(name, hint)) do
598624
%{
599-
kind: :function,
625+
kind: :export,
600626
name: name,
601627
arity: arity
602628
}
603629
end
604630
|> Enum.sort_by(&{&1.name, &1.arity})
605631
end
606632

607-
defp match_map_fields(map, hint) do
608-
for {key, value} when is_atom(key) <- Map.to_list(map),
609-
key = Atom.to_string(key),
610-
String.starts_with?(key, hint) do
611-
%{kind: :map_key, name: key, value_is_map: is_map(value)}
612-
end
613-
|> Enum.sort_by(& &1.name)
614-
end
615-
616-
defp get_module_funs(mod) do
633+
defp get_and_match_module_defs(mod, hint, exact?) do
617634
cond do
618635
not ensure_loaded?(mod) ->
619636
[]
620637

621638
docs = get_docs(mod, [:function, :macro]) ->
622639
exports(mod)
640+
|> Enum.filter(fn {fun, _} ->
641+
name = Atom.to_string(fun)
642+
if exact?, do: name == hint, else: String.starts_with?(name, hint)
643+
end)
623644
|> Kernel.--(default_arg_functions_with_doc_false(docs))
624-
|> Enum.reject(&hidden_fun?(&1, docs))
645+
|> Enum.sort()
646+
|> Enum.flat_map(&decorate_definition(&1, docs))
625647

626648
true ->
627-
exports(mod)
649+
mod
650+
|> exports()
651+
|> match_exports(hint, exact?)
628652
end
629653
end
630654

@@ -683,11 +707,20 @@ defmodule IEx.Autocomplete do
683707
do: {fun_name, new_arity}
684708
end
685709

686-
defp hidden_fun?({name, arity}, docs) do
687-
case Enum.find(docs, &match?({{_, ^name, ^arity}, _, _, _, _}, &1)) do
688-
nil -> match?([?_ | _], Atom.to_charlist(name))
689-
{_, _, _, :hidden, _} -> true
690-
{_, _, _, _, _} -> false
710+
defp decorate_definition({fun, arity}, docs) do
711+
case Enum.find(docs, &match?({{_, ^fun, ^arity}, _, _, _, _}, &1)) do
712+
nil ->
713+
case Atom.to_string(fun) do
714+
"_" <> _ -> []
715+
name -> [%{kind: :export, name: name, arity: arity}]
716+
end
717+
718+
{_, _, _, :hidden, _} ->
719+
[]
720+
721+
{_, _, _, _, metadata} ->
722+
group = metadata[:group] || (metadata[:guard] && "Guards") || "Exports"
723+
[%{kind: :export, name: Atom.to_string(fun), arity: arity, group: group}]
691724
end
692725
end
693726

@@ -696,46 +729,46 @@ defmodule IEx.Autocomplete do
696729

697730
## Ad-hoc conversions
698731

699-
defp to_entries(%{kind: :function, name: name, arity: arity}) do
700-
["#{name}/#{arity}"]
732+
defp to_entry(%{kind: :export, name: name, arity: arity}) do
733+
~c"#{name}/#{arity}"
701734
end
702735

703-
defp to_entries(%{kind: :sigil, name: name}) do
704-
["~#{name} (sigil_#{name})"]
736+
defp to_entry(%{kind: :sigil, name: name}) do
737+
~c"~#{name} (sigil_#{name})"
705738
end
706739

707-
defp to_entries(%{kind: :keyword, name: name}) do
708-
["#{name}:"]
740+
defp to_entry(%{kind: :keyword, name: name}) do
741+
~c"#{name}:"
709742
end
710743

711-
defp to_entries(%{kind: _, name: name}) do
712-
[name]
744+
defp to_entry(%{kind: _, name: name}) do
745+
String.to_charlist(name)
713746
end
714747

715748
# Add extra character only if pressing tab when done
716749
defp to_hint(%{kind: :module, name: hint}, hint) do
717-
"."
750+
~c"."
718751
end
719752

720753
defp to_hint(%{kind: :map_key, name: hint, value_is_map: true}, hint) do
721-
"."
754+
~c"."
722755
end
723756

724757
defp to_hint(%{kind: :file, name: hint}, hint) do
725-
"\""
758+
~c"\""
726759
end
727760

728761
# Add extra character whenever possible
729762
defp to_hint(%{kind: :dir, name: name}, hint) do
730-
format_hint(name, hint) <> "/"
763+
format_hint(name, hint) ++ ~c"/"
731764
end
732765

733766
defp to_hint(%{kind: :struct, name: name}, hint) do
734-
format_hint(name, hint) <> "{"
767+
format_hint(name, hint) ++ ~c"{"
735768
end
736769

737770
defp to_hint(%{kind: :keyword, name: name}, hint) do
738-
format_hint(name, hint) <> ": "
771+
format_hint(name, hint) ++ ~c": "
739772
end
740773

741774
defp to_hint(%{kind: _, name: name}, hint) do
@@ -744,7 +777,10 @@ defmodule IEx.Autocomplete do
744777

745778
defp format_hint(name, hint) do
746779
hint_size = byte_size(hint)
747-
binary_part(name, hint_size, byte_size(name) - hint_size)
780+
781+
name
782+
|> binary_part(hint_size, byte_size(name) - hint_size)
783+
|> String.to_charlist()
748784
end
749785

750786
## Evaluator interface

lib/iex/test/iex/autocomplete_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ defmodule IEx.AutocompleteTest do
185185
assert expand(~c":ets.fun2") == {:yes, ~c"ms", []}
186186
end
187187

188+
test "function completion with groups" do
189+
{:yes, ~c"", [exports, guards]} = expand(~c"Kernel.i")
190+
assert %{title: ~c"Exports", elems: [~c"if/2", ~c"inspect/1", ~c"inspect/2"]} = exports
191+
assert %{title: ~c"Guards", elems: [_ | _]} = guards
192+
193+
{:yes, ~c"", [guards, exports]} = expand(~c"Kernel.in")
194+
assert %{title: ~c"Guards", elems: [~c"in/2"]} = guards
195+
assert %{title: ~c"Exports", elems: [~c"inspect/1", ~c"inspect/2"]} = exports
196+
end
197+
188198
test "function completion with arity" do
189199
assert expand(~c"String.printable?") == {:yes, ~c"", [~c"printable?/1", ~c"printable?/2"]}
190200
assert expand(~c"String.printable?/") == {:yes, ~c"", [~c"printable?/1", ~c"printable?/2"]}

0 commit comments

Comments
 (0)