From abc79ec7ab813107a33a644679ffed19db919a3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?=
 <przemyslaw.wojtasik@erlang-solutions.com>
Date: Sat, 2 Apr 2022 21:32:49 +0200
Subject: [PATCH 1/4] Stop using logger

---
 config/config.exs             |  9 ---------
 lib/gradient.ex               |  4 +---
 lib/gradient/ast_specifier.ex | 15 +++------------
 3 files changed, 4 insertions(+), 24 deletions(-)
 delete mode 100644 config/config.exs

diff --git a/config/config.exs b/config/config.exs
deleted file mode 100644
index d7659338..00000000
--- a/config/config.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-import Config
-
-config :logger,
-  backends: [:console],
-  compile_time_purge_matching: [
-    [level_lower_than: :error]
-  ]
-
-config :logger, :console, format: "[$level] $message\n"
diff --git a/lib/gradient.ex b/lib/gradient.ex
index e8bae2fc..623830d8 100644
--- a/lib/gradient.ex
+++ b/lib/gradient.ex
@@ -15,8 +15,6 @@ defmodule Gradient do
   alias Gradient.AstSpecifier
   alias Gradient.ElixirChecker
 
-  require Logger
-
   @type options() :: [{:app_path, String.t()}, {:code_path, String.t()}]
 
   @spec type_check_file(String.t(), options()) :: :ok | :error
@@ -44,7 +42,7 @@ defmodule Gradient do
           _ -> :ok
         end
       error ->
-        Logger.error("Can't load file - #{inspect(error)}")
+        IO.puts(IO.ANSI.format([:red, "Can't load file - #{inspect(error)}"]))
         :error
     end
   end
diff --git a/lib/gradient/ast_specifier.ex b/lib/gradient/ast_specifier.ex
index 585c2881..50db9820 100644
--- a/lib/gradient/ast_specifier.ex
+++ b/lib/gradient/ast_specifier.ex
@@ -9,8 +9,6 @@ defmodule Gradient.AstSpecifier do
 
   import Gradient.Tokens
 
-  require Logger
-
   alias Gradient.Types
 
   @type token :: Types.token()
@@ -398,14 +396,7 @@ defmodule Gradient.AstSpecifier do
       when elem(skip, 0) in [
              :fun,
              :attribute,
-             :var,
-             nil,
-             :atom,
-             :char,
-             :float,
-             :integer,
-             :string,
-             :bin
+             :var
            ] do
     # NOTE fun - I skipped here checking &name/arity or &module.name/arity
     # skip forms that don't need analysis and do not display warning
@@ -413,7 +404,7 @@ defmodule Gradient.AstSpecifier do
   end
 
   def mapper(form, tokens, _opts) do
-    Logger.warn("Not found mapper for #{inspect(form)}")
+    IO.puts(IO.ANSI.format([:yellow, "Not found mapper for #{inspect(form)}"]))
     pass_tokens(form, tokens)
   end
 
@@ -512,7 +503,7 @@ defmodule Gradient.AstSpecifier do
         {[[g] | gs], ts}
 
       gs, {ags, ts} ->
-        Logger.error("Unsupported guards format #{inspect(gs)}")
+        IO.puts(IO.ANSI.format([:red, "Unsupported guards format #{inspect(gs)}"]))
         {gs ++ ags, ts}
     end)
   end

From 9f26b1cfa5eea3c1d9848fc6e9a77db7b02642b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?=
 <przemyslaw.wojtasik@erlang-solutions.com>
Date: Fri, 15 Apr 2022 09:33:44 +0200
Subject: [PATCH 2/4] wip

---
 lib/gradient/ast_specifier.ex | 119 ++++++++++++++++++++++++----------
 lib/gradient/elixir_fmt.ex    |  36 +++++++---
 lib/gradient/tokens.ex        |  51 +++++++++++++++
 3 files changed, 165 insertions(+), 41 deletions(-)

diff --git a/lib/gradient/ast_specifier.ex b/lib/gradient/ast_specifier.ex
index 50db9820..ef31ff69 100644
--- a/lib/gradient/ast_specifier.ex
+++ b/lib/gradient/ast_specifier.ex
@@ -21,6 +21,8 @@ defmodule Gradient.AstSpecifier do
   # Expressions that could have missing location
   @lineless_forms [:atom, :char, :float, :integer, :string, :bin, :cons, :tuple]
 
+  @ensure_location [:var | @lineless_forms]
+
   # Api
 
   @doc """
@@ -255,7 +257,7 @@ defmodule Gradient.AstSpecifier do
     |> get_tuple(opts)
     |> case do
       {:tuple, tokens} ->
-        {anno, opts} = update_line_from_tokens(tokens, anno, opts)
+        {anno, opts} = update_loc_from_tokens(tokens, anno, opts)
         # drop a token that begins tuple
         tokens = drop_tokens_while(tokens, fn t -> elem(t, 0) in [:"{"] end)
 
@@ -323,17 +325,38 @@ defmodule Gradient.AstSpecifier do
     |> pass_tokens(tokens)
   end
 
-  def mapper({:call, anno, name, args}, tokens, opts) do
+  def mapper({:call, anno, name, args} = c, tokens, opts) do
     # anno has correct line
-    {:ok, _, anno, opts, _} = get_line(anno, opts)
+    {:ok, _, _, opts, _} = get_line(anno, opts)
     name = remote_mapper(name)
 
+    {{:call, anno, _, _}, _tokens} = specify_line(c, tokens, opts)
+
+    anno =
+      if is_integer(anno) do
+        anno
+      else
+        set_end_location(anno, get_closing_paren_loc(tokens))
+      end
+
     {args, tokens} = call_args_mapper(args, tokens, name, opts)
 
     {:call, anno, name, args}
     |> pass_tokens(tokens)
   end
 
+  def set_end_location(anno, :undefined), do: anno
+  # def set_end_location(anno, location) when is_integer(anno) do
+  # [location: anno, end_location: location]
+  # end
+  def set_end_location(anno, location) when is_tuple(anno) do
+    [location: anno, end_location: location]
+  end
+
+  def set_end_location(anno, location) do
+    Keyword.put(anno, :end_location, location)
+  end
+
   def mapper({:op, anno, op, left, right}, tokens, opts) do
     # anno has correct line
     {:ok, _, anno, opts, _} = get_line(anno, opts)
@@ -383,7 +406,7 @@ defmodule Gradient.AstSpecifier do
   end
 
   def mapper({type, anno, value}, tokens, opts)
-      when type in @lineless_forms do
+      when type in @ensure_location do
     {:ok, line} = Keyword.fetch(opts, :line)
     anno = :erl_anno.set_line(line, anno)
     anno = :erl_anno.set_generated(Keyword.get(opts, :generated, false), anno)
@@ -395,8 +418,7 @@ defmodule Gradient.AstSpecifier do
   def mapper(skip, tokens, _opts)
       when elem(skip, 0) in [
              :fun,
-             :attribute,
-             :var
+             :attribute
            ] do
     # NOTE fun - I skipped here checking &name/arity or &module.name/arity
     # skip forms that don't need analysis and do not display warning
@@ -543,7 +565,7 @@ defmodule Gradient.AstSpecifier do
   def cons_mapper({:cons, anno, value, tail}, tokens, opts) do
     {:ok, _, anno0, opts0, _} = get_line(anno, opts)
 
-    {anno, opts} = update_line_from_tokens(tokens, anno0, opts0)
+    {anno, opts} = update_loc_from_tokens(tokens, anno0, opts0)
     # drop a token that begins list
     tokens = drop_tokens_while(tokens, fn t -> elem(t, 0) in [:"["] end)
 
@@ -662,59 +684,90 @@ defmodule Gradient.AstSpecifier do
     l2 <= l1
   end
 
+  defp match_token_to_form({:identifier, {l1, _, _}, t_name}, {:var, l2, raw_name}) do
+    l2 == l1 && String.contains?(to_string(raw_name), <<to_string(t_name)::binary, "@">>)
+  end
+
+  defp match_token_to_form({:paren_identifier, {l1, _, _}, name1}, {:call, l2, name2, _}) do
+    :erl_anno.line(l2) == l1 && match_remote_names(name1, name2)
+  end
+
   defp match_token_to_form(_, _) do
     false
   end
 
+  def match_remote_names(name, {:remote, _, _, {:atom, _, name}}), do: true
+  def match_remote_names(name, {:atom, _, name}), do: true
+  def match_remote_names(name, name), do: true
+  def match_remote_names(_, _), do: false
+
+  defp literal_loc({line, col_start, _}, literal) do
+    col_end = col_start + length(to_charlist(literal))
+    [location: {line, col_start}, end_location: {line, col_end}]
+  end
+
   @spec take_loc_from_token(token(), form()) :: form()
-  defp take_loc_from_token({:int, {line, _, _}, _}, {:integer, _, value}) do
-    {:integer, line, value}
+  defp take_loc_from_token({:int, loc, _}, {:integer, _, value}) do
+    {:integer, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:char, {line, _, _}, _}, {:integer, _, value}) do
-    {:integer, line, value}
+  defp take_loc_from_token({:char, loc, _}, {:integer, _, value}) do
+    {:integer, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:flt, {line, _, _}, _}, {:float, _, value}) do
-    {:float, line, value}
+  defp take_loc_from_token({:flt, loc, _}, {:float, _, value}) do
+    {:float, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:atom, {line, _, _}, _}, {:atom, _, value}) do
-    {:atom, line, value}
+  defp take_loc_from_token({:atom, loc, _}, {:atom, _, value}) do
+    {:atom, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:alias, {line, _, _}, _}, {:atom, _, value}) do
-    {:atom, line, value}
+  defp take_loc_from_token({:alias, loc, _}, {:atom, _, value}) do
+    {:atom, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:kw_identifier, {line, _, _}, _}, {:atom, _, value}) do
-    {:atom, line, value}
+  defp take_loc_from_token({:kw_identifier, loc, _}, {:atom, _, value}) do
+    {:atom, literal_loc(loc, value), value}
   end
 
-  defp take_loc_from_token({:list_string, {l1, _, _}, _}, {:cons, _, _, _} = charlist) do
-    charlist_set_loc(charlist, l1)
+  defp take_loc_from_token({:list_string, loc, v}, {:cons, _, _, _} = charlist) do
+    charlist_set_loc(charlist, literal_loc(loc, v))
   end
 
   defp take_loc_from_token(
-         {:bin_string, {l1, _, _}, _},
+         {:bin_string, loc, _},
          {:bin, _, [{:bin_element, _, {:string, _, v2}, :default, :default}]}
        ) do
-    {:bin, l1, [{:bin_element, l1, {:string, l1, v2}, :default, :default}]}
+    loc = literal_loc(loc, v2)
+    {:bin, loc, [{:bin_element, loc, {:string, loc, v2}, :default, :default}]}
   end
 
   defp take_loc_from_token({:str, _, _}, {:string, loc, v2}) do
+    # FIXME missing col
     {:string, loc, v2}
   end
 
-  defp take_loc_from_token({true, {line, _, _}}, {:atom, _, true}) do
-    {:atom, line, true}
+  defp take_loc_from_token({true, loc}, {:atom, _, true}) do
+    {:atom, literal_loc(loc, true), true}
   end
 
-  defp take_loc_from_token({false, {line, _, _}}, {:atom, _, false}) do
-    {:atom, line, false}
+  defp take_loc_from_token({false, loc}, {:atom, _, false}) do
+    {:atom, literal_loc(loc, true), false}
   end
 
-  defp take_loc_from_token(_, _), do: nil
+  defp take_loc_from_token({:identifier, loc, name}, {:var, _, raw_name}) do
+    {:var, literal_loc(loc, name), raw_name}
+  end
+
+  defp take_loc_from_token({:paren_identifier, {line, col, _}, _}, {:call, anno, name, args}) do
+    {:call, :erl_anno.set_location({line, col}, anno), name, args}
+  end
+
+  defp take_loc_from_token(t, e) do
+    IO.puts(IO.ANSI.format([:red, "Cannot take loc from token - #{inspect(t)}, #{inspect(e)}"]))
+    nil
+  end
 
   def cons_to_charlist({nil, _}), do: []
 
@@ -728,12 +781,12 @@ defmodule Gradient.AstSpecifier do
 
   def charlist_set_loc({nil, _}, loc), do: {nil, loc}
 
-  def update_line_from_tokens([token | _], anno, opts) do
-    line = get_line_from_token(token)
-    {:erl_anno.set_line(line, anno), Keyword.put(opts, :line, line)}
+  def update_loc_from_tokens([token | _], anno, opts) do
+    {line, col} = get_loc_from_token(token)
+    {:erl_anno.set_location({line, col}, anno), Keyword.put(opts, :line, line)}
   end
 
-  def update_line_from_tokens(_, anno, opts) do
+  def update_loc_from_tokens(_, anno, opts) do
     {anno, opts}
   end
 
@@ -763,7 +816,7 @@ defmodule Gradient.AstSpecifier do
   end
 
   defp set_form_end_line(opts, form, forms) do
-    if elem(form, 0) not in [:bin, :cons] do
+    if elem(form, 0) not in [:bin] do
       set_form_end_line_(opts, form, forms)
     else
       opts
diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex
index 7c20bfe4..91ee4b38 100644
--- a/lib/gradient/elixir_fmt.ex
+++ b/lib/gradient/elixir_fmt.ex
@@ -306,12 +306,12 @@ defmodule Gradient.ElixirFmt do
 
   @spec highlight_in_context(tuple(), [String.t()], options()) :: iodata()
   def highlight_in_context(expression, context, opts) do
-    line = elem(expression, 1)
+    anno = elem(expression, 1)
 
     context
     |> Enum.with_index(1)
-    |> filter_context(line, 2)
-    |> underscore_line(line, opts)
+    |> filter_context(anno, 2)
+    |> underscore_line(anno, opts)
     |> Enum.join("\n")
   end
 
@@ -322,18 +322,35 @@ defmodule Gradient.ElixirFmt do
     Enum.filter(lines, fn {_, number} -> number in range end)
   end
 
-  def underscore_line(lines, line, opts) do
+  def end_location(anno) when is_list(anno) do
+    Keyword.get(anno, :end_location, :undefined)
+  end
+  def end_location(_anno)  do
+    :undefined
+  end
+
+  def underscore_line(lines, anno, opts) do
+    line = :erl_anno.line(anno)
+    column = :erl_anno.column(anno)
+    IO.inspect(column, label: "COLUMN")
+    endl = end_location(anno)
+    IO.inspect(endl, label: "END LOCATION")
+
     Enum.map(lines, fn {str, n} ->
       if(n == line) do
         colors = get_colors_with_default(opts)
         {:ok, use_colors} = Keyword.fetch(colors, :use_colors)
         {:ok, color} = Keyword.fetch(colors, :underscored_line)
-        line_str = to_string(n) <> " " <> str
+        {bef, aft} = split_at_col(str, column)
+        indent = to_string(n) <> " " <> bef
 
         [
-          IO.ANSI.underline(),
-          IO.ANSI.format_fragment([color, line_str], use_colors),
-          IO.ANSI.reset()
+          indent,
+          [
+            IO.ANSI.underline(),
+            IO.ANSI.format_fragment([color, aft], use_colors),
+            IO.ANSI.reset()
+          ]
         ]
       else
         to_string(n) <> " " <> str
@@ -341,6 +358,9 @@ defmodule Gradient.ElixirFmt do
     end)
   end
 
+  def split_at_col(str, col) when is_integer(col), do: String.split_at(str, col - 1)
+  def split_at_col(str, _), do: {"", str}
+
   def get_ex_file_path([{:attribute, 1, :file, {path, 1}} | _]), do: {:ok, path}
   def get_ex_file_path(_), do: {:error, :not_found}
 
diff --git a/lib/gradient/tokens.ex b/lib/gradient/tokens.ex
index 42f99ce5..9d1ff14b 100644
--- a/lib/gradient/tokens.ex
+++ b/lib/gradient/tokens.ex
@@ -117,6 +117,12 @@ defmodule Gradient.Tokens do
     end)
   end
 
+  @doc """
+  Get location from token.
+  """
+  @spec get_loc_from_token(T.token()) :: {integer(), integer()}
+  def get_loc_from_token(token), do: {elem(elem(token, 1), 0), elem(elem(token, 1), 1)}
+
   @doc """
   Get line from token.
   """
@@ -159,8 +165,53 @@ defmodule Gradient.Tokens do
     |> Enum.concat()
   end
 
+  @spec select_tokens_in_paren(T.tokens(), T.line()) :: T.tokens()
+  def select_tokens_in_paren(tokens, line) do
+    tokens
+    |> drop_tokens_to_line(line)
+    |> find_opening_paren()
+    |> find_closing_paren()
+  end
+
+  def get_closing_paren_loc(tokens, line \\ 0) do
+    case select_tokens_in_paren(tokens, line) do
+      [] ->
+        :undefined
+
+      list ->
+        {line, col, _} = elem(List.last(list), 1)
+        {line, col}
+    end
+  end
+
   # Private
 
+  defp find_opening_paren(tokens) do
+    drop_tokens_while(tokens, fn
+      {:"(", _} -> false
+      _ -> true
+    end)
+  end
+
+  defp find_closing_paren(tokens, init \\ 0)
+  defp find_closing_paren([], _), do: []
+
+  defp find_closing_paren(tokens, init) do
+    Enum.reduce_while(tokens, {[], init}, fn
+      {:")", _} = t, {ts, 1} -> {:halt, [t | ts]}
+      {:")", _} = t, {ts, open} when open > 1 -> {:cont, {[t | ts], open - 1}}
+      {:"(", _} = t, {ts, open} -> {:cont, {[t | ts], open + 1}}
+      t, {ts, open} -> {:cont, {[t | ts], open}}
+    end)
+    |> case do
+      {_, _open} ->
+        raise "Cannot find closing paren in given tokens"
+
+      list ->
+        list
+    end
+  end
+
   defp flatten_token(token) do
     case token do
       {:bin_string, _, [s]} = t when is_binary(s) ->

From 51e66f4ae4ebe533d00bc02bfbf13547e4a82715 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?=
 <przemyslaw.wojtasik@erlang-solutions.com>
Date: Fri, 15 Apr 2022 20:03:40 +0200
Subject: [PATCH 3/4] Make formatter able to highlight more than one line

---
 lib/gradient/anno.ex       | 40 ++++++++++++++++
 lib/gradient/elixir_fmt.ex | 94 +++++++++++++++++++++++++-------------
 2 files changed, 101 insertions(+), 33 deletions(-)
 create mode 100644 lib/gradient/anno.ex

diff --git a/lib/gradient/anno.ex b/lib/gradient/anno.ex
new file mode 100644
index 00000000..c001bd8d
--- /dev/null
+++ b/lib/gradient/anno.ex
@@ -0,0 +1,40 @@
+defmodule Gradient.Anno do
+  @type anno :: keyword()
+  @type location :: {non_neg_integer(), pos_integer()}
+  @type line :: non_neg_integer()
+
+  @max_col 1000
+
+  @spec end_location(anno()) :: location()
+  def end_location(anno) when is_list(anno) do
+    case Keyword.fetch(anno, :end_location) do
+      {:ok, {line, col}} -> {abs_line(line(anno), line), col}
+      :error -> line(anno)
+    end
+  end
+
+  def end_location(anno), do: {line(anno), @max_col}
+
+  def end_line(anno) when is_list(anno) do
+    case Keyword.fetch(anno, :end_location) do
+      {:ok, {line, _}} -> abs_line(line(anno), line)
+      :error -> line(anno)
+    end
+  end
+
+  def end_line(anno), do: line(anno)
+
+  @spec line(anno()) :: line()
+  def line(anno), do: :erl_anno.line(:erl_anno.from_term(anno))
+
+  @spec location(anno()) :: location()
+  def location(anno) do
+    case :erl_anno.location(:erl_anno.from_term(anno)) do
+      {line, col} -> {line, col}
+      line -> {line, 1}
+    end
+  end
+
+  def abs_line(startl, endl) when startl > endl, do: 2 * startl - endl
+  def abs_line(_, endl), do: endl
+end
diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex
index 91ee4b38..e3b1bfc1 100644
--- a/lib/gradient/elixir_fmt.ex
+++ b/lib/gradient/elixir_fmt.ex
@@ -23,6 +23,7 @@ defmodule Gradient.ElixirFmt do
   alias Gradient.ElixirType
   alias Gradient.ElixirExpr
   alias Gradient.Types
+  alias Gradient.Anno
 
   @type colors_opts() :: [
           use_colors: boolean(),
@@ -311,54 +312,81 @@ defmodule Gradient.ElixirFmt do
     context
     |> Enum.with_index(1)
     |> filter_context(anno, 2)
-    |> underscore_line(anno, opts)
+    |> maybe_underscore_lines(anno, opts)
     |> Enum.join("\n")
   end
 
-  def filter_context(lines, loc, ctx_size \\ 1) do
-    line = :erl_anno.line(loc)
-    range = (line - ctx_size)..(line + ctx_size)
+  def filter_context(lines, anno, ctx_size \\ 1) do
+    line = Anno.line(anno)
+    end_line = Anno.end_line(anno)
+    range = (line - ctx_size)..(end_line + ctx_size)
 
     Enum.filter(lines, fn {_, number} -> number in range end)
   end
 
-  def end_location(anno) when is_list(anno) do
-    Keyword.get(anno, :end_location, :undefined)
-  end
-  def end_location(_anno)  do
-    :undefined
-  end
-
-  def underscore_line(lines, anno, opts) do
-    line = :erl_anno.line(anno)
-    column = :erl_anno.column(anno)
-    IO.inspect(column, label: "COLUMN")
-    endl = end_location(anno)
-    IO.inspect(endl, label: "END LOCATION")
+  def maybe_underscore_lines(lines, anno, opts) do
+    Anno.location(anno) |> IO.inspect(label: "START LOC")
+    Anno.end_location(anno) |> IO.inspect(label: "END LOC")
 
     Enum.map(lines, fn {str, n} ->
-      if(n == line) do
+      if need_underscore?(n, anno) do
         colors = get_colors_with_default(opts)
-        {:ok, use_colors} = Keyword.fetch(colors, :use_colors)
-        {:ok, color} = Keyword.fetch(colors, :underscored_line)
-        {bef, aft} = split_at_col(str, column)
-        indent = to_string(n) <> " " <> bef
-
-        [
-          indent,
-          [
-            IO.ANSI.underline(),
-            IO.ANSI.format_fragment([color, aft], use_colors),
-            IO.ANSI.reset()
-          ]
-        ]
+        underscore_line(str, n, anno, colors)
       else
-        to_string(n) <> " " <> str
+        [to_string(n), " ", str]
       end
     end)
   end
 
-  def split_at_col(str, col) when is_integer(col), do: String.split_at(str, col - 1)
+  def underscore_line(str, n, anno, colors) do
+    {start_line, start_col} = Anno.location(anno)
+    {end_line, end_col} = Anno.end_location(anno)
+
+    case n do
+      l when l == start_line and l == end_line ->
+        {prefix, str} = split_at_col(str, start_col)
+        {str, suffix} = split_at_col(str, end_col - start_col)
+        [prefix, make_underscore(str, colors), suffix]
+
+      ^start_line ->
+        {prefix, str} = split_at_col(str, start_col)
+        [prefix, make_underscore(str, colors)]
+
+      ^end_line ->
+        {str, suffix} = split_at_col(str, end_col)
+        [indent, str] = separate_indent(str)
+        [indent, make_underscore(str, colors), suffix]
+
+      _otherwise ->
+        separate_indent(str)
+    end
+    |> add_line_number(n)
+  end
+
+  def add_line_number(iolist, n), do: [to_string(n), " ", iolist]
+
+  def separate_indent(str) do
+    trim_str = String.trim(str)
+    indent = gen_indent(String.length(str) - String.length(trim_str))
+    [indent, trim_str]
+  end
+
+  def gen_indent(length), do: Stream.cycle(' ') |> Stream.take(length) |> Enum.to_list()
+
+  def make_underscore(text, colors) do
+    {:ok, use_colors} = Keyword.fetch(colors, :use_colors)
+    {:ok, color} = Keyword.fetch(colors, :underscored_line)
+
+    [
+      IO.ANSI.underline(),
+      IO.ANSI.format_fragment([color, text], use_colors),
+      IO.ANSI.reset()
+    ]
+  end
+
+  def need_underscore?(index, anno), do: index >= Anno.line(anno) && index <= Anno.end_line(anno)
+
+  def split_at_col(str, col) when is_integer(col), do: String.split_at(str, col)
   def split_at_col(str, _), do: {"", str}
 
   def get_ex_file_path([{:attribute, 1, :file, {path, 1}} | _]), do: {:ok, path}

From 4816ebe67cae16e9103398e43203492addb106a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?=
 <przemyslaw.wojtasik@erlang-solutions.com>
Date: Fri, 20 May 2022 15:15:28 +0200
Subject: [PATCH 4/4] wip

---
 .tool-versions                                |  2 +-
 examples/simple_app/.tool-versions            |  1 +
 examples/simple_app/lib/simple_app/pipe_op.ex | 29 +++++++++++++++++++
 lib/gradient/elixir_fmt.ex                    |  1 +
 test/gradient/ast_specifier_test.exs          | 22 +++++++-------
 test/gradient/elixir_fmt_test.exs             |  2 +-
 6 files changed, 44 insertions(+), 13 deletions(-)
 create mode 100644 examples/simple_app/.tool-versions
 create mode 100644 examples/simple_app/lib/simple_app/pipe_op.ex

diff --git a/.tool-versions b/.tool-versions
index 24efada7..df042d56 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
-elixir 1.12.3
+elixir 1.13.3
 erlang 24.1
diff --git a/examples/simple_app/.tool-versions b/examples/simple_app/.tool-versions
new file mode 100644
index 00000000..a5704253
--- /dev/null
+++ b/examples/simple_app/.tool-versions
@@ -0,0 +1 @@
+elixir 1.11.4
diff --git a/examples/simple_app/lib/simple_app/pipe_op.ex b/examples/simple_app/lib/simple_app/pipe_op.ex
new file mode 100644
index 00000000..9c92e6c2
--- /dev/null
+++ b/examples/simple_app/lib/simple_app/pipe_op.ex
@@ -0,0 +1,29 @@
+defmodule SimpleApp.PipeOp do
+
+  use Gradient.TypeAnnotation
+
+  @spec int_inc(integer()) :: integer()
+  def int_inc(int) do
+    int + 1
+  end
+
+  def easy_pipe do
+    '1'
+    |> int_inc()
+    '1'
+    |> int_inc()
+    '1'
+    |> int_inc()
+    '1'
+    |> int_inc()
+  end
+
+  def easy_pipe2 do
+    int_inc(
+      {%{a: 1, b: 2, c: 3},
+      %{a: 1, b: 2, c: 3},
+      %{a: 1, b: 2, c: 3}})
+
+  end
+
+end
diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex
index e3b1bfc1..3932ee5b 100644
--- a/lib/gradient/elixir_fmt.ex
+++ b/lib/gradient/elixir_fmt.ex
@@ -308,6 +308,7 @@ defmodule Gradient.ElixirFmt do
   @spec highlight_in_context(tuple(), [String.t()], options()) :: iodata()
   def highlight_in_context(expression, context, opts) do
     anno = elem(expression, 1)
+    IO.inspect(expression)
 
     context
     |> Enum.with_index(1)
diff --git a/test/gradient/ast_specifier_test.exs b/test/gradient/ast_specifier_test.exs
index 0d47d76d..7242378a 100644
--- a/test/gradient/ast_specifier_test.exs
+++ b/test/gradient/ast_specifier_test.exs
@@ -37,7 +37,7 @@ defmodule Gradient.AstSpecifierTest do
 
       [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse()
 
-      assert {:function, 2, :int, 0, [{:clause, 2, [], [], [{:integer, 2, 1}]}]} = inline
+      assert {:function, 2, :int, 0, [{:clause, 2, [], [], [{:integer, [location: {2, 16}, end_location: {2, 17}], 1}]}]} = inline
 
       assert {:function, 4, :int_block, 0, [{:clause, 4, [], [], [{:integer, 5, 2}]}]} = block
     end
@@ -46,7 +46,7 @@ defmodule Gradient.AstSpecifierTest do
       {tokens, ast} = load("Elixir.Basic.Float.beam", "basic/float.ex")
 
       [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse()
-      assert {:function, 2, :float, 0, [{:clause, 2, [], [], [{:float, 2, 0.12}]}]} = inline
+      assert {:function, 2, :float, 0, [{:clause, 2, [], [], [{:float, [location: {2, 18}, end_location: {2, 22}], 0.12}]}]} = inline
 
       assert {:function, 4, :float_block, 0, [{:clause, 4, [], [], [{:float, 5, 0.12}]}]} = block
     end
@@ -56,9 +56,9 @@ defmodule Gradient.AstSpecifierTest do
 
       [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse()
 
-      assert {:function, 2, :atom, 0, [{:clause, 2, [], [], [{:atom, 2, :ok}]}]} = inline
+      assert {:function, 2, :atom, 0, [{:clause, 2, [], [], [{:atom, [location: {2, 17}, end_location: {2, 19}], :ok}]}]} = inline
 
-      assert {:function, 4, :atom_block, 0, [{:clause, 4, [], [], [{:atom, 5, :ok}]}]} = block
+      assert {:function, 4, :atom_block, 0, [{:clause, 4, [], [], [{:atom, [location: {5, 5}, end_location: {5, 7}], :ok}]}]} = block
     end
 
     test "char" do
@@ -516,10 +516,10 @@ defmodule Gradient.AstSpecifierTest do
     {tokens, _} = example_data()
     opts = [end_line: -1]
 
-    assert {{:integer, 21, 12}, tokens} =
+    assert {{:integer, [location: {21, 9}, end_location: {21, 11}], 12}, tokens} =
              AstSpecifier.specify_line({:integer, 21, 12}, tokens, opts)
 
-    assert {{:integer, 22, 12}, _tokens} =
+    assert {{:integer, [location: {22, 5}, end_location: {22, 7}], 12}, _tokens} =
              AstSpecifier.specify_line({:integer, 20, 12}, tokens, opts)
   end
 
@@ -1043,8 +1043,8 @@ defmodule Gradient.AstSpecifierTest do
                [
                  {:map, 7,
                   [
-                    {:map_field_assoc, 7, {:atom, 7, :a}, {:integer, 7, 12}},
-                    {:map_field_assoc, 7, {:atom, 7, :b}, {:call, 7, {:atom, 7, :empty_map}, []}}
+                    {:map_field_assoc, 7, {:atom, {:atom, [location: {7, 7}, end_location: {7, 8}], :a}, :a}, {:integer, {:atom, [location: {7, 10}, end_location: {7, 12}], :a}, 12}},
+                    {:map_field_assoc, 7, {:atom, [location: {7, 14}, end_location: {7, 15}], :b}, {:call, [location: {7, 17}, end_location: {7, 27}], {:atom, 7, :empty_map}, []}}
                   ]}
                ]}
             ]} = test_map
@@ -1633,13 +1633,13 @@ defmodule Gradient.AstSpecifierTest do
     {tokensB, astB} = load("Elixir.NestedModules.ModuleB.beam", "nested_modules.ex")
     {tokens, ast} = load("Elixir.NestedModules.beam", "nested_modules.ex")
 
-    assert {:function, 3, :name, 0, [{:clause, 3, [], [], [{:atom, 4, :module_a}]}]} =
+    assert {:function, 3, :name, 0, [{:clause, 3, [], [], [{:atom, [location: {4, 7}, end_location: {4, 15}], :module_a}]}]} =
              List.last(AstSpecifier.run_mappers(astA, tokensA))
 
-    assert {:function, 9, :name, 0, [{:clause, 9, [], [], [{:atom, 10, :module_b}]}]} =
+    assert {:function, 9, :name, 0, [{:clause, 9, [], [], [{:atom, [location: {10, 7}, end_location: {10, 15}], :module_b}]}]} =
              List.last(AstSpecifier.run_mappers(astB, tokensB))
 
-    assert {:function, 14, :name, 0, [{:clause, 14, [], [], [{:atom, 15, :module}]}]} =
+    assert {:function, 14, :name, 0, [{:clause, 14, [], [], [{:atom, [location: {15, 5}, end_location: {15, 11}], :module}]}]} =
              List.last(AstSpecifier.run_mappers(ast, tokens))
   end
 
diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs
index b70f0a5e..551d57a1 100644
--- a/test/gradient/elixir_fmt_test.exs
+++ b/test/gradient/elixir_fmt_test.exs
@@ -21,7 +21,7 @@ defmodule Gradient.ElixirFmtTest do
     res = ElixirFmt.try_highlight_in_context(expression, opts)
 
     expected =
-      {:ok, "29   def bool_id(x) do\n30     _x = 13\n\e[4m\e[31m31     12\e[0m\n32   end\n33 "}
+      {:ok, "29   def bool_id(x) do\n30     _x = 13\n31\e[4m\e[31m     12\e[0m\n32   end\n33 "}
 
     assert res == expected
   end