Giter VIP home page Giter VIP logo

next-ls's People

Contributors

aayushmau5 avatar akirak avatar biletskyy avatar crbelaus avatar dependabot[bot] avatar dvic avatar fhunleth avatar github-actions[bot] avatar goofansu avatar ismaelga avatar jamesvl avatar jjcarstens avatar jozuas avatar lucacervello avatar lud-wj avatar mhanberg avatar mikayla-maki avatar njichev avatar philipgiuliani avatar robozoom avatar shanesveller avatar stefanluptak avatar wkirschbaum avatar zachallaun avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

next-ls's Issues

use ast traversal to compute workspace symbols

workspace symbols should be computed the same way as document symbols.

The full ast from Code.string_to_quoted works better for now, if I can upstream some enhancements to the data stored in the Dbgi chunk, we might be able to go back to the data from the tracer.

NextLS crashes when the project deps updates

After running mix deps.update --all on a project using NextLS, the NextLS server crashes with the following error:

[NextLS] NextLS v0.5.4 has initialized!
[NextLS] Booting runtime...
[NextLS] Unchecked dependencies for environment dev:
[NextLS] * flop (Hex package)
[NextLS]   the dependency does not match the requirement "~> 0.22.0", got "0.21.0"
* flop_phoenix (Hex package)
[NextLS]   the dependency does not match the requirement "~> 0.21.0", got "0.19.1"
[NextLS] ** (Mix) Can't continue due to errors on dependencies
[NextLS] The runtime has crashed

After deleting and the .elixir-tools directory it recompiles and works again.

go to definition not working

I am trying out 0.6.4 on my work computer now and go to definition is not working, it seems not all of the references are being generated.

will investigate.

Dialyzer Extension

  • Extension that computes and broadcasts Dialyzer diagnostics
  • only activates if Dialyxir is detected in the runtime (Dialyxir is obviously not necessary for using dialyzer, but it signals intent from the user that they use dialyzer and would like to see in editor diagnostics)
  • Code actions?

completion

Features

Non exhaustive list of completion candiates

  • Modules
  • remote functions
  • local functions
  • private functions
  • variables/parameters
  • module attributes
  • structs
  • struct fields
  • bitstring modifiers
  • special forms
  • kernel functions
  • imported functions (eg, import Foo)
  • aliased modules (eg, alias Foo.Bar or alias Foo.Bar, as: Baz)

More features will be unlocked and made more robust as we work through elixir-lang/elixir#12645

Should not execute config/runtime.exs

Description

This was noticed by a user who used a dependency in their config/runtime.exs file, which was not loaded yet because their app was not compiled at all.

But, they also showed that by using functions like System.fetch_env!/1, the config will fail and the app will fail to start.

Solution

Need to find a way to start the runtime without it executing any of the Application config files.

Optimize table lookups

In larger projects, Go To Definition can feel a little slow.

This can be optimized by creating an ets table from the dets table, and reading from that.

prompt to run mix deps.get

if you don't have dependencies installed, the editor should prompt you to install them and accept a yes or no.

this should avoid having to leave the editor, run mix deps.get, and open it again.

rebuild symbol table

in case the symbol table (used for workspace/document symbols and references/gotodef) gets funky, you can rebuild it.

Decide what symbols to hide from workspace symbols

Description

Many modules include "public private" functions that aren't intended for you to use and are injected with metaprogramming.

This functions often are prefixed and suffixed with a double underscore __struct__. These seem to clutter up the symbol table and make it harder to use.

I believe that IEx will hide those from autocomplete, so it might make sense to filter those in the symbol table.

Example

image

execute compiler rpc in a task

this will not block the runtime genserver, so that stdout packets from the port can be forwarded to the LSP process and logged correctly.

it currently synchronously compiles, and then logs the pile of log messages.

when it fires off a task (should be a supervised task), it should set some state to lock the server so that other processes can't ask it for data assuming the code is compiled

or a flag to say its compiling, and only allow rpc's that don't care about that

Is formatting compatible with "mix format"?

When working with vscode-elixir-ls and the editor.formatOnSave = true option, quite often you'll end up with format-only changes that are different from the ones brought by mix format. This makes it a bit painful to work with vscode-elixir-ls in that regard (you either re-run mix format, or hand-pick the right changes when committing with git etc).

See: elixir-lsp/vscode-elixir-ls#224

With next-ls, will this problem remain the same? It would be a great improvement if the formatting was directly compatible with mix format.

TextDocumentDidSave with optional string `text' crashes the server

For TextDocumentDidSave, the server will crash when the text property is nil, which can be nil according to the spec: https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#textDocument_didSave.

Potential fix:

@@ -337,7 +337,7 @@ defmodule NextLS do
 
     {:noreply,
      lsp
-     |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n")))
+     |> then(&put_in(&1.assigns.documents[uri], String.split(text || "", "\n")))
      |> then(&put_in(&1.assigns.refresh_refs[task.ref], {token, "Compiled!"}))}
   end

error publishing diagnostics

11:00:06.874 [error] LSP Exited.

Last message received: handle_info :publish

** (MatchError) no match of right hand side value: {:error, %{params: %{diagnostics: "expected a list of a %GenLSP.Structures.Diagnostic{}"}}}
    (gen_lsp 0.1.2) lib/gen_lsp.ex:305: GenLSP.dump!/2
    (gen_lsp 0.1.2) lib/gen_lsp.ex:209: GenLSP.notify/2
    (next_ls 0.1.1) lib/next_ls.ex:252: anonymous fn/3 in NextLS.handle_info/2
    (stdlib 4.3) maps.erl:411: :maps.fold_1/3
    (next_ls 0.1.1) lib/next_ls.ex:251: NextLS.handle_info/2
    (gen_lsp 0.1.2) lib/gen_lsp.ex:277: anonymous fn/4 in GenLSP.loop/3
    (gen_lsp 0.1.2) lib/gen_lsp.ex:289: GenLSP.attempt/2
    (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3




11:00:06.876 [error] LSP Exited.

Last message received: handle_info {#Reference<0.2346312572.3890020356.4244>, [%Mix.Task.Compiler.Diagnostic{file: "<redacted>", severity: :warning, message: ["<redacted>", " is undefined (module ", "<redacted>", " is not available or is yet to be defined)"], position: 51, compiler_name: "Elixir", details: nil}, %Mix.Task.Compiler.Diagnostic{file: "<redacted>", severity: :warning, message: ["<redacted>", " is undefined (module ", "<redacted>", " is not available or is yet to be defined)"], position: 50, compiler_name: "Elixir", details: nil}, %Mix.Task.Compiler.Diagnostic{file: "<redacted>", severity: :warning, message: ["<redacted>", " is undefined (module ", "<redacted>", " is not available or is yet to be defined)"], position: 49, compiler_name: "Elixir", details: nil}]}

** (MatchError) no match of right hand side value: {:error, %{params: %{diagnostics: "expected a list of a %GenLSP.Structures.Diagnostic{}"}}}
    (gen_lsp 0.1.2) lib/gen_lsp.ex:305: GenLSP.dump!/2
    (gen_lsp 0.1.2) lib/gen_lsp.ex:209: GenLSP.notify/2
    (next_ls 0.1.1) lib/next_ls.ex:252: anonymous fn/3 in NextLS.handle_info/2
    (stdlib 4.3) maps.erl:411: :maps.fold_1/3
    (next_ls 0.1.1) lib/next_ls.ex:251: NextLS.handle_info/2
    (gen_lsp 0.1.2) lib/gen_lsp.ex:277: anonymous fn/4 in GenLSP.loop/3
    (gen_lsp 0.1.2) lib/gen_lsp.ex:289: GenLSP.attempt/2
    (stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Schematic should also output a better error than this.

figure out nextension api

libraries will be able to provide their own nextensions (next-ls extensions ๐Ÿ˜Ž), and they should be able to provide their own hover, completions, code actions, commands, etc.

the api currently enables broadcasting diagnostics (seen in the builtin ElixirExtension and soon to be Credo and Dialyzer extensions), so this needs to be prototyped.

also need to figure out the distribution mechanism for nextensions

also need to figure out an api for when a nextension should activate.

Features

  • diagnostics
  • code actions
  • completions
  • document symbols (breadcrumbs, outline)
  • code lens
  • hover

[Error] ** (ArithmeticError) bad argument in arithmetic expression

I assume this has to do with document symbols in some way.

Triggered by the following file
defmodule Schematic do
  @external_resource "README.md"
  @moduledoc "README.md"
             |> File.read!()
             |> String.split("<!-- MDOC !-->")
             |> Enum.fetch!(1)

  defstruct [:unify, :kind, :message, :meta]

  @typedoc """
  The Schematic data structure.

  This data structure is meant to be opaque to the user, but you can create your own for super niche use cases. But backwards compatiblility of this data structure is not guaranteed.
  """
  @opaque t :: %__MODULE__{
            unify: (term(), :up | :down -> {:ok, term()} | {:error, String.t() | [String.t()]}),
            kind: String.t(),
            message: function() | nil
          }

  @typedoc """
  A lazy reference to a schematic, used to define recursive schematics.
  """
  @type lazy_schematic :: {atom(), atom(), list(any())}

  @typedoc """
  A literal schematic.
  """
  @type literal :: any()

  unless macro_exported?(Kernel, :then, 2) do
    defmacrop then(value, fun) do
      quote do
        unquote(fun).(unquote(value))
      end
    end
  end

  defmodule OptionalKey do
    @enforce_keys [:key]
    defstruct [:key]

    @opaque t :: %__MODULE__{
              key: {any(), any()} | any()
            }
  end

  defmodule Error do
    @moduledoc false
    defstruct []
  end

  @doc """
  Specifies that the data can be **any**thing.

  ## Usage

  ```elixir
  iex> schematic = any()
  iex> {:ok, "hi!"} = unify(schematic, "hi!")
  iex> {:ok, [:one, :two, :three]} = unify(schematic, [:one, :two, :three])
  iex> {:ok, true} = unify(schematic, true)

"""
@spec any() :: t()
def any() do
%Schematic{kind: "any", unify: telemetry_wrap(:any, %{}, fn x, _dir -> {:ok, x} end)}
end

@doc """
Shortcut for specifiying that a schematic can be either null or the schematic.

Usage

iex> schematic = nullable(str())
iex> {:ok, nil} = unify(schematic, nil)
iex> {:ok, "hi!"} = unify(schematic, "hi!")
iex> {:error, "expected either null or a string"} = unify(schematic, :boom)

"""
@spec nullable(t() | lazy_schematic() | literal()) :: t()
def nullable(schematic) do
oneof([nil, schematic])
end

@doc """
Specifies that the data is a boolean or a specific boolean.

Usage

Any boolean.

iex> schematic = bool()
iex> {:ok, true} = unify(schematic, true)
iex> {:ok, false} = unify(schematic, false)
iex> {:error, "expected a boolean"} = unify(schematic, :boom)

A boolean literal.

iex> schematic = true
iex> {:ok, true} = unify(schematic, true)
iex> {:error, "expected true"} = unify(schematic, :boom)

"""
@spec bool() :: t()
def bool() do
message = fn ->
"a boolean"
end

%Schematic{
  kind: "boolean",
  message: message,
  unify:
    telemetry_wrap(:bool, %{}, fn input, _dir ->
      if is_boolean(input) do
        {:ok, input}
      else
        {:error, "expected #{message.()}"}
      end
    end)
}

end

@doc """
Specifies that the data is a string or a specific string.

Usage

Any string.

iex> schematic = str()
iex> {:ok, "hi!"} = unify(schematic, "hi!")
iex> {:error, "expected a string"} = unify(schematic, :boom)

A string literal.

iex> schematic = "I ๐Ÿ’œ Elixir"
iex> {:ok, "I ๐Ÿ’œ Elixir"} = unify(schematic,  "I ๐Ÿ’œ Elixir")
iex> {:error, ~s|expected "I ๐Ÿ’œ Elixir"|} = unify(schematic, "I love Ruby")

"""
@spec str() :: t()
def str() do
message = fn ->
"a string"
end

%Schematic{
  kind: "string",
  message: message,
  unify:
    telemetry_wrap(:str, %{}, fn input, _dir ->
      if is_binary(input) do
        {:ok, input}
      else
        {:error, "expected #{message.()}"}
      end
    end)
}

end

@doc """
Specifies that the data is an integer or a specific integer.

Usage

Any integer.

iex> schematic = int()
iex> {:ok, 99} = unify(schematic, 99)
iex> {:error, "expected an integer"} = unify(schematic, :boom)

A integer literal.

iex> schematic = 99
iex> {:ok, 99} = unify(schematic,  99)
iex> {:error, ~s|expected 99|} = unify(schematic, :ninetynine)

"""
@spec int() :: t()
def int() do
message = fn ->
"an integer"
end

%Schematic{
  kind: "integer",
  message: message,
  unify:
    telemetry_wrap(:int, %{}, fn input, _dir ->
      if is_integer(input) do
        {:ok, input}
      else
        {:error, "expected #{message.()}"}
      end
    end)
}

end

@doc """
Specifies that the data is a float or specific float.

Usage

Any float

iex> schematic = float()
iex> {:ok, 99.0} = unify(schematic, 99.0)
iex> {:error, "expected a float"} = unify(schematic, :boom)

A float literal.

iex> schematic = 99.0
iex> {:ok, 99.0} = unify(schematic,  99.0)
iex> {:error, ~s|expected 99.0|} = unify(schematic, :ninetynine)

"""
@spec float() :: t()
def float() do
message = fn ->
"a float"
end

%Schematic{
  kind: "float",
  message: message,
  unify:
    telemetry_wrap(:float, %{}, fn input, _dir ->
      if is_float(input) do
        {:ok, input}
      else
        {:error, "expected #{message.()}"}
      end
    end)
}

end

@doc """
Specifies that the data is a list of any size and contains anything.

Usage

iex> schematic = list()
iex> {:ok, ["one", 2, :three]} = unify(schematic, ["one", 2, :three])
iex> {:error, "expected a list"} = unify(schematic, :hi)

"""
@spec list() :: t()
def list() do
message = fn -> "a list" end

%Schematic{
  kind: "list",
  message: message,
  unify:
    telemetry_wrap(:list, %{}, fn input, _dir ->
      if is_list(input) do
        {:ok, input}
      else
        {:error, ~s|expected #{message.()}|}
      end
    end)
}

end

@doc """
Specifies that the data is a list whose items unify to the given schematic.

Lists whose elements do not unify return a list of :ok and :error tuples.

Usage

iex> schematic = list(oneof([str(), int()]))
iex> {:ok, ["one", 2, "three"]} = unify(schematic, ["one", 2, "three"])
iex> {:error, [ok: "one", ok: 2, error: "expected either a string or an integer"]} = unify(schematic, ["one", 2, :three])

"""
@spec list(t() | lazy_schematic() | literal()) :: t()
def list(schematic) do
schematic = fn ->
case schematic do
{mod, func, args} ->
apply(mod, func, args)

    schematic ->
      schematic
  end
end

message = fn -> "a list of #{schematic.().message.()}" end

%Schematic{
  kind: "list",
  message: message,
  unify:
    telemetry_wrap(:list, %{}, fn input, dir ->
      if is_list(input) do
        Enum.map_reduce(input, [], fn el, acc ->
          case schematic.().unify.(el, dir) do
            {:ok, output} -> {output, [{:ok, output} | acc]}
            {:error, error} -> {%Error{}, [{:error, error} | acc]}
          end
        end)
        |> then(fn {result, errors} ->
          if %Error{} in result do
            {:error, Enum.reverse(errors)}
          else
            {:ok, result}
          end
        end)
      else
        {:error, ~s|expected a list|}
      end
    end)
}

end

@doc """
Specifies that the data is a tuple of the given length where each element unifies to the schematic in the same position.

Usage

iex> schematic = tuple([str(), int()])
iex> {:ok, {"one", 2}} = unify(schematic, {"one", 2})
iex> {:error, "expected a tuple of {a string, an integer}"} = unify(schematic, {1, "two"})

Options

  • :from - Either :tuple or :list. Defaults to :tuple.
iex> schematic = tuple([str(), int()], from: :list)
iex> {:ok, {"one", 2}} = unify(schematic, ["one", 2])
iex> {:error, "expected a list of {a string, an integer}"} = unify(schematic, [1, "two"])

"""
@spec tuple([t() | lazy_schematic() | literal()], Keyword.t()) :: t()
def tuple(schematics, opts \ []) do
from = Keyword.get(opts, :from, :tuple)

message = fn ->
  "a #{from} of {#{Enum.map_join(schematics, ", ", &Schematic.Unification.message/1)}}"
end

{condition, to_list, length} =
  case from do
    :list ->
      {&is_list/1, &Function.identity/1, &Enum.count/1}

    :tuple ->
      {&is_tuple/1, &Tuple.to_list/1, &tuple_size/1}
  end

%Schematic{
  kind: "tuple",
  message: message,
  unify: fn input, dir ->
    if condition.(input) and length.(input) == Enum.count(schematics) do
      input
      |> to_list.()
      |> Enum.with_index()
      |> Enum.reduce_while({:ok, []}, fn {el, idx}, {:ok, acc} ->
        case Schematic.Unification.unify(Enum.at(schematics, idx), el, dir) do
          {:ok, output} ->
            {:cont, {:ok, [output | acc]}}

          {:error, _error} ->
            {:halt, {:error, ~s|expected #{message.()}|}}
        end
      end)
      |> then(fn
        {:ok, result} ->
          {:ok, result |> Enum.reverse() |> List.to_tuple()}

        error ->
          error
      end)
    else
      {:error, ~s|expected #{message.()}|}
    end
  end
}

end

@doc """
Specifies that the data is a map with the given keys (literal values) that unify to the provided blueprint.

Unification errors for keys are returned in a map with the key as the key and the value as the error.

  • Map schematics serve as a way to permit certain keys and discard all others.
  • Keys are non-nullable unless the value schematic is marked with nullable/1. This allows the value of the key to be nil as well as the key to be absent from the source data.
  • Keys are considered required unless tagged with optional/1. This allows the entire key to be absent from the source data. If the key is present, it must unify according to the given schematic.

Basic Usage

The most basic map schematic can look like the following.

iex> schematic = map(%{
...>   "league" => oneof(["NBA", "MLB", "NFL"]),
...> })
iex> # ignores the `"team"` key
iex> {:ok, %{"league" => "NBA"}} == unify(schematic, %{"league" => "NBA", "team" => "Chicago Bulls"})
true
iex> {:error,
...>   %{
...>     "league" =>
...>     ~s|expected either "NBA", "MLB", or "NFL"|
...>   }} = unify(schematic, %{"league" => "NHL"})

With a permissive amp

If you want to only check that the data is a map, but not the shape, you can use map/0.

iex> schematic = map()
iex> {:ok, %{"league" => "NBA"}} = unify(schematic, %{"league" => "NBA"})

With nullable/1

Marking a key as nullable using nullable/1.

This means the value of the key can be nil as well as omitting the key entirely. The unified output will always contain the key.

iex> schematic = map(%{
...>   "title" => str(),
...>   "description" => nullable(str())
...> })
iex> {:ok, %{"title" => "Elixir 101", "description" => nil}} = unify(schematic, %{"title" => "Elixir 101", "description" => nil})
iex> {:ok, %{"title" => "Elixir 101", "description" => nil}} = unify(schematic, %{"title" => "Elixir 101"})
iex> {:ok, %{"title" => "Elixir 101", "description" => nil}} = dump(schematic, %{"title" => "Elixir 101"})

With optional/1

Marking a key as optional using optional/1.

This means that you can omit the key from the input and that the unified output will not contain the key if it wasn't in the input.

If the key is provided, it must unify according to the given schematic.

Likewise, using dump/2 will also omit that key.

iex> schematic = map(%{
...>   "title" => str(),
...>   optional("description") => str()
...> })
iex> {:ok, %{"title" => "Elixir 101", "description" =>  "An amazing programming course."}} = unify(schematic, %{"title" => "Elixir 101", "description" => "An amazing programming course."})
iex> {:ok, %{"title" => "Elixir 101"}} = unify(schematic, %{"title" => "Elixir 101"})
iex> {:ok, %{"title" => "Elixir 101"}} = dump(schematic, %{"title" => "Elixir 101"})

With :keys and :values

Instead of passing a blueprint, which specifies keys and values, you can pass a :keys and :values options which provide schematic that all keys and values in the input must unify to.

iex> schematic = map(keys: str(), values: oneof([str(), int()]))
iex> {:ok, %{"type" => "big", "quantity" => 99}} = unify(schematic, %{"type" => "big", "quantity" => 99})
iex> {:error, %{"quantity" => "expected either a string or an integer"}} = unify(schematic, %{"type" => "big", "quantity" => [99]})

Transforming Keys

During unification, key transformation can be performed if it is specified in the schematic.

You can specify a key as a 2-tuple with the first element being the input key and the second element being the output key. When calling dump/2, the key will be turned from the output key back to the input key (and will also be revalidated).

This is useful for transforming string keys to atom keys as well as camelCase keys to snake_case keys.

Key transformation can also be used when declaring an optional key with optional/1.

iex> schematic = map(%{
...>   {"teamName", :team_name} => str()
...> })
iex> {:ok, %{team_name: "Chicago Bulls"}} = unify(schematic, %{"teamName" => "Chicago Bulls"})
iex> {:ok, %{"teamName" => "Chicago Bulls"}} = dump(schematic, %{team_name: "Chicago Bulls"})

Recursive Schematics

One can define schematics that specify keys whose values are themselves.

For this to be possible, recursive schematics must terminate some way. This can be achienved by specifying those keys as optional/1 or within a oneof/1 schematic.

Recursive schematics are specified as a MFA tuple, t:lazy_schematic/0.

iex> defmodule Tree do
...>   import Schematic
...>
...>   def schematic() do
...>     map(%{values: list(Tree.branch())})
...>   end
...>
...>   def branch() do
...>     map(%{
...>       values: list(oneof([Tree.leaf(), {__MODULE__, :branch, []}]))
...>     })
...>   end
...>
...>   def leaf() do
...>     map(%{
...>       value: str()
...>     })
...>   end
...> end
iex> input = %{
...>   type: "root",
...>   values: [
...>     %{
...>       type: "branch",
...>       values: [
...>         %{
...>           type: "leaf",
...>           value: "i'm a leaf"
...>         },
...>         %{
...>           type: "branch",
...>           values: [
...>             %{
...>               type: "leaf",
...>               value: "i'm another leaf"
...>             }
...>           ]
...>         }
...>       ]
...>     }
...>   ]
...> }
iex> unify(SchematicTest.Tree.schematic(), input)
{:ok, %{values: [%{values: [%{value: "i'm a leaf"}, %{values: [%{value: "i'm another leaf"}]}]}]}}

"""

@typedoc "Map blueprint key."
@type map_blueprint_key :: OptionalKey.t() | any()

@typedoc "Map blueprint value."
@type map_blueprint_value :: t() | lazy_schematic() | literal()

@typedoc """
The blueprint used to specify a map schematic.
"""
@type map_blueprint :: %{map_blueprint_key() => map_blueprint_value()}

@spec map(%{map_blueprint_key() => map_blueprint_value()} | Keyword.t()) :: t()
def map(blueprint_or_opts \ [])

def map(blueprint) when is_map(blueprint) do
%Schematic{
kind: "map",
message: fn -> "a map" end,
meta: %{blueprint: blueprint},
unify:
telemetry_wrap(:map, %{style: :blueprint}, fn input, dir ->
if is_map(input) do
bp_keys = Map.keys(blueprint)

        Enum.reduce(
          bp_keys,
          [ok: %{}, errors: %{}],
          fn bpk, [{:ok, acc}, {:errors, errors}] ->
            schematic =
              case blueprint[bpk] do
                {mod, func, args} ->
                  apply(mod, func, args)

                schematic ->
                  schematic
              end

            key = with %OptionalKey{key: key} <- bpk, do: key
            {from_key, to_key} = with key when not is_tuple(key) <- key, do: {key, key}

            {from_key, to_key} =
              case dir do
                :to -> {from_key, to_key}
                :from -> {to_key, from_key}
              end

            if not Map.has_key?(input, from_key) and match?(%OptionalKey{}, bpk) do
              [{:ok, acc}, {:errors, errors}]
            else
              case Schematic.Unification.unify(schematic, input[from_key], dir) do
                {:ok, output} ->
                  acc =
                    acc
                    |> Map.put(to_key, output)

                  [{:ok, acc}, {:errors, errors}]

                {:error, error} ->
                  # NOTE: in the case of schemas, an optional key will exist because structs always
                  # have all of their fields. So if they don't unify **and**, the key is optional, and
                  # the value is nil, we can assume the schematic did not allow nil, and we can omit
                  # the key from the dump.
                  if input[from_key] == nil and match?(%OptionalKey{}, bpk) do
                    [{:ok, acc}, {:errors, errors}]
                  else
                    [{:ok, acc}, {:errors, Map.put(errors, from_key, error)}]
                  end
              end
            end
          end
        )
        |> then(fn
          [ok: output, errors: e] when map_size(e) == 0 ->
            {:ok, output}

          [ok: _output, errors: errors] ->
            {:error, errors}
        end)
      else
        {:error, "expected a map"}
      end
    end)
}

end

def map(opts) when is_list(opts) do
key_schematic = Keyword.get(opts, :keys, any())
value_schematic = Keyword.get(opts, :values, any())

%Schematic{
  kind: "map",
  message: fn -> "a map" end,
  unify:
    telemetry_wrap(:map, %{style: :open}, fn input, dir ->
      if is_map(input) do
        Enum.reduce(
          Map.keys(input),
          [ok: %{}, errors: %{}],
          fn input_key, [{:ok, acc}, {:errors, errors}] ->
            case key_schematic.unify.(input_key, dir) do
              {:ok, key_output} ->
                case value_schematic.unify.(input[input_key], dir) do
                  {:ok, value_output} ->
                    [{:ok, Map.put(acc, key_output, value_output)}, {:errors, errors}]

                  {:error, error} ->
                    [{:ok, acc}, {:errors, Map.put(errors, input_key, error)}]
                end

              {:error, _error} ->
                # NOTE: we pass just ignore keys which non conforming keys
                [{:ok, acc}, {:errors, errors}]
            end
          end
        )
        |> then(fn
          [ok: output, errors: e] when map_size(e) == 0 ->
            {:ok, output}

          [ok: _output, errors: errors] ->
            {:error, errors}
        end)
      else
        {:error, "expected a map"}
      end
    end)
}

end

@doc """
Map schematics can be extended by using map/2.

iex> player = map(%{name: str(), team: str()})
iex> baseball_player = map(player, %{home_runs: int()})
iex> unify(baseball_player, %{name: "Sammy Sosa", team: "Cubs", home_runs: 609, favorite_food: "Hot Dog"})
{:ok, %{name: "Sammy Sosa", team: "Cubs", home_runs: 609}}

"""
@spec map(t(), map()) :: t()
def map(schematic, blueprint) do
map(Map.merge(schematic.meta.blueprint, blueprint))
end

@doc """
Specifies a map/1 schematic that is then hydrated into a struct.

Works the same as the map/1 schematic, but will also automatically transform all keys from string keys to atom keys if a key conversion is not already specified.

Since this schematic hydrates a struct, it is also only capable of having atom keys in the output, whereas a normal map can have arbitrary terms as the key.

iex> schematic =
...>   schema(HTTPRequest, %{
...>     method: oneof(["POST", "PUT", "PATCH"]),
...>     body: str()
...>   })
iex> {:ok, %HTTPRequest{method: "POST", body: ~s|{"name": "Peter"}|}} = unify(schematic, %{"method" => "POST", "body" => ~s|{"name": "Peter"}|})
iex> {:ok, %{"method" => "POST", "body" => ~s|{"name": "Peter"}|}} = dump(schematic, %HTTPRequest{method: "POST", body: ~s|{"name": "Peter"}|})

"""

@typedoc "Schema blueprint key."
@type schema_blueprint_key :: OptionalKey.t() | atom()

@typedoc "Schema blueprint value."
@type schema_blueprint_value :: t() | lazy_schematic() | literal()

@typedoc """
The blueprint used to specify a schema schematic.
"""
@type schema_blueprint :: %{schema_blueprint_key() => schema_blueprint_value()}

@spec schema(atom(), schema_blueprint()) :: t()
def schema(mod, blueprint) do
schematic =
map(
Map.new(blueprint, fn
{%OptionalKey{key: k}, v} when is_atom(k) ->
{%OptionalKey{key: {to_string(k), k}}, v}

      {k, v} when is_atom(k) ->
        {{to_string(k), k}, v}

      kv ->
        kv
    end)
  )

%Schematic{
  kind: "#{mod}",
  message: fn -> "a %#{String.replace(to_string(mod), "Elixir.", "")}{}" end,
  unify:
    telemetry_wrap(:schema, %{mod: mod}, fn input, dir ->
      case dir do
        :to ->
          with {:ok, output} <- schematic.unify.(input, :to) do
            {:ok, struct(mod, output)}
          end

        :from ->
          with {:ok, input} <- struct?(input, mod),
               {:ok, output} <- schematic.unify.(Map.from_struct(input), :from) do
            {:ok, output}
          end
      end
    end)
}

end

defp struct?(%struct{} = input, mod) when struct == mod do
{:ok, input}
end

defp struct?(_input, mod) do
{:error, "expected a #{mod} struct"}
end

@doc """
A utility for creating custom schematics.

The raw/1 schematic is useful for creating schematics that unify the values of the inputs, rather than just the shape.

Options

  • :message - a custom error message. Defaults to "is invalid".
  • :transformer - a function that takes the input and the unification direction and must return the desired value. Defaults to fn input, _dir -> input end.

Basic Usage

iex> schematic = all([int(), raw(fn i -> i > 10 end, message: "must be greater than 10")])
iex> {:ok, 11} = unify(schematic, 11)
iex> {:error, ["must be greater than 10"]} = unify(schematic, 9)

Advanced Usage

If your data requires different validations for unification and dumping, then you can pass a 2-arity function (instead of a 1-arity function) and the second parameter will be the direction.

This concept also applies to the :transform option.

iex> schematic =
...>   raw(
...>     fn
...>       n, :to -> is_list(n) and length(n) == 3
...>       n, :from -> is_tuple(n) and tuple_size(n) == 3
...>     end,
...>     message: "must be a tuple of size 3",
...>     transform: fn
...>       input, :to ->
...>         List.to_tuple(input)
...>       input, :from ->
...>         Tuple.to_list(input)
...>     end
...>   )
iex> {:ok, {"one", "two", 3}} = unify(schematic, ["one", "two", 3])
iex> {:error, "must be a tuple of size 3"} = unify(schematic, ["not", "big"])
iex> {:ok, ["one", "two", 3]} = dump(schematic, {"one", "two", 3})

"""
@spec raw((any() -> boolean()) | (any(), :up | :down -> boolean()), [tuple()]) :: t()
def raw(function, opts \ []) do
message = fn -> Keyword.get(opts, :message, "is invalid") end
transformer = Keyword.get(opts, :transform, fn input, _dir -> input end)

%Schematic{
  kind: "function",
  message: message,
  unify:
    telemetry_wrap(:raw, %{}, fn input, dir ->
      if convert_to_two_arity(function).(input, dir) do
        {:ok, convert_to_two_arity(transformer).(input, dir)}
      else
        {:error, message.()}
      end
    end)
}

end

defp convert_to_two_arity(f) when is_function(f, 1) do
fn a, _ -> f.(a) end
end

defp convert_to_two_arity(f) when is_function(f, 2) do
f
end

@doc """
Specifies that the data must unify with all of the given schematics.

On error, returns a list of validation messages.

If a schematic raises an exception, it is caught and the error "is invalid" is returned.

iex> schematic = all([int(), raw(&Kernel.<(&1, 10), message: "must be less than 10"), raw(&(Kernel.rem(&1, 2) == 0), message: "must be divisible by 2")])
iex> {:ok, 8} = unify(schematic, 8)
iex> {:error, ["must be less than 10", "must be divisible by 2"]} = unify(schematic, 15)
iex> {:error, ["expected an integer", "must be less than 10", "is invalid"]} = unify(schematic, "15")

"""
@spec all([t()]) :: t()
def all(schematics) when is_list(schematics) do
message = fn -> Enum.map(schematics, & &1.message) end

%Schematic{
  kind: "all",
  message: message,
  unify:
    telemetry_wrap(:all, %{}, fn input, dir ->
      errors =
        for schematic <- schematics,
            {result, message} =
              __try__(fn -> Schematic.Unification.unify(schematic, input, dir) end),
            result == :error do
          message
        end

      if Enum.empty?(errors) do
        {:ok, input}
      else
        {:error, errors}
      end
    end)
}

end

defp try(callback) do
callback.()
rescue
_ ->
{:error, "is invalid"}
end

@doc """
Specifies that the data unifies to one of the given schematics.

Can be called with a list of schematics or a function.

With a list

When called with a list of schematics, they will be traversed during unification and the first one to unify will be returned. If none of them unify, then an error is returned.

iex> team = map(%{name: str(), league: str()})
iex> player = map(%{name: str(), team: str()})
iex> schematic = oneof([team, player])
iex> {:ok, %{name: "Indiana Pacers", league: "NBA"}} = unify(schematic, %{name: "Indiana Pacers", league: "NBA"})
iex> {:ok, %{name: "George Hill", team: "Indiana Pacers"}} = unify(schematic, %{name: "George Hill", team: "Indiana Pacers"})
iex> {:error, "expected either a map or a map"} = unify(schematic, %{name: "NBA", sport: "basketball"})

With a function

When called with a function, the input is passed as the only parameter. This can be used to dispach to a specific schematic. This is a performance optimization, as you can dispatch to a specific schematic rather than traversing all of them.

iex> schematic = oneof(fn
...>   %{type: "team"} -> map(%{name: str(), league: str()})
...>   %{type: "player"} -> map(%{name: str(), team: str()})
...>   _ -> {:error, "expected either a player or a team"}
...> end)
iex> {:ok, %{name: "Indiana Pacers", league: "NBA"}} = unify(schematic, %{type: "team", name: "Indiana Pacers", league: "NBA"})
iex> {:ok, %{name: "George Hill", team: "Indiana Pacers"}} = unify(schematic, %{type: "player", name: "George Hill", team: "Indiana Pacers"})
iex> {:error, "expected either a player or a team"} = unify(schematic, %{name: "NBA", sport: "basketball"})

"""
@spec oneof([t() | lazy_schematic() | literal()] | (any -> t() | literal())) :: t()
def oneof(schematics) when is_list(schematics) do
message = fn ->
"either #{sentence_join(schematics, "or", &Schematic.Unification.message/1)}"
end

%Schematic{
  kind: "oneof",
  message: message,
  unify:
    telemetry_wrap(:oneof, %{style: :sequential}, fn input, dir ->
      inquiry =
        Enum.find_value(schematics, fn schematic ->
          schematic =
            case schematic do
              {mod, func, args} -> apply(mod, func, args)
              schematic -> schematic
            end

          with {:error, _} <- Schematic.Unification.unify(schematic, input, dir), do: false
        end)

      with nil <- inquiry, do: {:error, ~s|expected #{message.()}|}
    end)
}

end

def oneof(dispatch) when is_function(dispatch) do
%Schematic{
kind: "oneof:dispatch",
unify:
telemetry_wrap(:oneof, %{style: :dispatch}, fn input, dir ->
with %Schematic{} = schematic <- dispatch.(input) do
Schematic.Unification.unify(schematic, input, dir)
end
end)
}
end

defp sentence_join(items, joiner, mapper) do
length = length(items)
item_joiner = if length > 2, do: ", ", else: " "

Enum.map_join(Enum.with_index(items), item_joiner, fn {item, idx} ->
  if idx == length - 1, do: joiner <> " " <> (mapper.(item) || ""), else: mapper.(item)
end)

end

@doc """
Unify external data with your internal data structures.

See all the other functions for information on how to create schematics.
"""
@spec unify(t() | literal(), any()) :: any()
def unify(schematic, input) do
Schematic.Unification.unify(schematic, input, :to)
end

@doc """
Dump your internal data to their external data structures.

See all the other functions for information on how to create schematics.
"""
@spec dump(t() | literal(), any()) :: any()
def dump(schematic, input) do
Schematic.Unification.unify(schematic, input, :from)
end

defp telemetry_wrap(type, metadata, func) do
fn input, dir ->
metadata = Map.merge(%{kind: type, dir: dir}, metadata)

  :telemetry.span([:schematic, :unify], metadata, fn ->
    result = func.(input, dir)
    {result, metadata}
  end)
end

end

@doc """
See map/1 for examples and explanation.
"""
@spec optional(any()) :: OptionalKey.t()
def optional(key) do
%OptionalKey{key: key}
end
end


</pre>
</details>

code actions

Tracking ticket, please comment with ideas for code actions!

  • mark unused variable with underscore
  • alias module
  • require module (like Logger)
  • extract variable
  • extract function
  • inline function
  • inline variable

Add crypto to extra applications

Since this application uses crypto, it should probably be added to the list of extra_applications.

2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "warning: :crypto.strong_rand_bytes/1 defined in application :crypto is used by the current application but the current application does not depend on :crypto. To fix this, you must do one of:\n"
2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "\n"
2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "  1. If :crypto is part of Erlang/Elixir, you must include it under :extra_applications inside \"def application\" in your mix.exs\n"
2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "\n"
2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "  2. If :crypto is a dependency, make sure it is listed under \"def deps\" in your mix.exs\n"
2023-07-07T19:32:59.543 helix_lsp::transport [ERROR] next-ls err <- "\n"
2023-07-07T19:32:59.544 helix_lsp::transport [ERROR] next-ls err <- "  3. In case you don't want to add a requirement to :crypto, you may optionally skip this warning by adding [xref: [exclude: [:crypto]]] to your \"def project\" in mix.exs\n"
2023-07-07T19:32:59.544 helix_lsp::transport [ERROR] next-ls err <- "\n"
2023-07-07T19:32:59.544 helix_lsp::transport [ERROR] next-ls err <- "  lib/next_ls.ex:508: NextLS.token/0\n"
2023-07-07T19:32:59.544 helix_lsp::transport [ERROR] next-ls err <- "\n"

to/from pipe

turn Enum.to_list(map) into map |> Enum.to_list() and vice versa.

cancel progress tokens for canceled tasks

Currently, if we are compiling, and send a progress begin update, but then we save and then cancel the in progress tasks, we aren't sending the progress end update for that tasks token.

Windows Support

I attempted to enable NextLS via the VSCode Extension, however the client was not created.

The first question is, in a general sense, is NextLS viable in windows?

If NextLS supports windows, but the initialization script is unsupported in windows, then there is an issue packaging and initializing it for windows users of VSCode.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.