Current section
Files
Jump to
Current section
Files
lib/eh.ex
defmodule Eh do
@shortdoc "Lookup Elixir documentation from the command line"
@moduledoc """
Lookup Elixir documentation for Elixir terms.
Eh works in the same way as Elixir's built-in IEx.Helpers.h, except that it
exposes that functionality as a mix task.
"""
# The main function has no @doc statement on purpose. The reason for this is
# that I want to be able to test functions that has no documentation
@doc false
def main([definition | _tail]), do: lookup(definition)
@doc """
Lookup and print documentation on an Elixir term.
This is intended to work like using `IEx.Helpers.h` in the `iex`
shell, but without actually firing up `iex`.
`definition` can be something like:
* `String`
* `String.to_integer`
* `IO.ANSI.Docs.print`
* `IO.ANSI.Docs.print/2`
"""
def lookup(definition) do
case docs(InputParser.parse(definition)) do
:no_docs -> IO.puts "No documentation for #{definition} was found"
{:not_found, mod} -> IO.puts "Could not load module #{mod}"
{:ok, docs} ->
for {term, doc} <- Enum.reverse(docs) do
IO.ANSI.Docs.print_heading(title(term), monochrome_colors)
IO.ANSI.Docs.print(doc, monochrome_colors)
end
end
end
@doc """
List all public functions of a module.
Examples:
iex> Eh.functions("Eh")
["Eh.main/1", "Eh.lookup/1", "Eh.functions/1"]
"""
def functions(module) do
module = Module.concat([module])
case Code.ensure_loaded?(module) do
true ->
Code.get_docs(module, :docs)
|> Enum.map(fn (doc) -> reformat_doc(module, doc) end)
|> Enum.map(fn ({term, _doc}) -> title(term) end)
false ->
[]
end
end
# Find documentation for the specified {mod, fun, arity} tuple.
#
# Returns:
# - {:not_found, mod} if the module does not exist
# - {:no_docs, mod} if the module exists, but does not have docs
# - {:ok, [doc...]} for all matching doc strings
defp docs({nil, nil, nil}),
do: :not_found
# Retrieve documentation for a Module
defp docs({mod, nil, nil}) do
case Code.get_docs(mod, :moduledoc) do
nil ->
{:not_found, mod}
{_, binary} when is_binary(binary) ->
doc = append_functions(mod, binary)
{:ok, [{{mod}, doc}]}
end
end
# Try to find function in Kernel if mod is nil
defp docs({nil, fun, arity}),
do: docs({Kernel, fun, arity})
# Retrieve documentation for a specific Module.function/arity
# arity may be nil
# The list comprehension here is borrowed from IEx.Introspection.
defp docs(term) do
case filter_docs(term) do
nil ->
{mod, _, _} = term
{:not_found, mod}
[] ->
:no_docs
results -> {:ok, results}
end
end
# Filter out the relevant documentation chunks we need.
#
# Only include entries from Code.get_docs(mod, :docs) that:
# 1. Have documentation
# 2. Matches our fun/arity filter parameters
# 3. If arity is nil, include everything that matches fun
defp filter_docs({mod, fun, arity}) do
case Code.get_docs(mod, :docs) do
nil ->
nil
results ->
results
|> Enum.filter(fn (doc) -> filter_docs({fun, arity}, doc) end)
|> Enum.map(fn (doc) -> reformat_doc(mod, doc) end)
end
end
defp filter_docs(_filter, {_, _, _, _, doc}) when doc in [nil, false],
do: false
defp filter_docs(filter, {filter, _ln, _tp, _args, _doc}),
do: true
defp filter_docs({fun, nil}, {{fun, _arity}, _ln, _tp, _args, _doc}),
do: true
defp filter_docs(_filter, _doc),
do: false
# Reformat Elixir's internal structure to {{"Module.function/arity", doc}}
defp reformat_doc(mod, {{fun, arity}, _ln, _tp, _args, doc}),
do: {{mod, fun, arity}, doc}
# Convert a {mod, fun, arity} tuple to a "Module.function/arity" string
defp title({mod}),
do: "#{mod}"
defp title({mod, fun, nil}),
do: "#{mod}.#{fun}"
defp title({mod, fun, arity}),
do: "#{mod}.#{fun}/#{arity}"
defp append_functions(mod, moduledoc) do
functions = functions(mod)
|> Enum.map(fn (fun) -> String.replace(fun, ~r(^.+\.), "") end)
|> Enum.map(fn(fun) -> "\n* #{fun}" end)
moduledoc <> """
Public functions:
""" <> Enum.join(functions)
end
# Custom colors, kept in monochrome without any specific color choices, to
# work well with both dark an bright terminals.
defp monochrome_colors do
[enabled: true,
doc_bold: [:bright],
doc_code: [:faint],
doc_headings: [:underline],
doc_inline_code: [:faint],
doc_table_heading: [:reverse],
doc_title: [:reverse, :bright],
doc_underline: [:underline],
width: 80]
end
end