elixir-tools / next-ls Goto Github PK
View Code? Open in Web Editor NEWThe language server for Elixir that just works. Ready for early adopters!
Home Page: https://www.elixir-tools.dev/next-ls
License: MIT License
The language server for Elixir that just works. Ready for early adopters!
Home Page: https://www.elixir-tools.dev/next-ls
License: MIT License
Tracking ticket for code lenses!
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.
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.
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.
I have installed https://github.com/elixir-tools/elixir-tools.vscode and enabled nextLS.
However, when I try to format the document, VSCode says no formatter installed. How do I get started with this?
Sorry, if the question is too trivial.
Non exhaustive list of completion candiates
import Foo
)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
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.
Need to find a way to start the runtime without it executing any of the Application config files.
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.
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.
in case the symbol table (used for workspace/document symbols and references/gotodef) gets funky, you can rebuild it.
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.
currently the main LSP process can get bogged down with log messages and it can't handle requests from the editor
this is an uncommon occurrence, but when installing for the first time, can be demonstrated
related to #60
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
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
.
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
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.
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.
I assume this has to do with document symbols in some way.
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 aoneof/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 usingmap/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 amap/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}
enddefp 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 tofn 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
enddefp 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)
}
enddefp 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)
enddefp 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 """
Seemap/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
[definition] local variables
Credo extension
- Extension that computes and broadcasts Credo diagnostics
- only activates if Credo is detected in the runtime
- Code actions from https://github.com/elixir-tools/credo-language-server
[definition] captured functions
compiler diagnostics for test files
go to definition
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"
Phoenix Extension
semantic tokens
to/from pipe
turn
Enum.to_list(map)
intomap |> 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.
parameter hints
[definition] dependency definitions
references: error when finding references for `self()`
It's not clear to me if this is a Neovim bug or a NextLS bug, but i will confirm in VSCode later
CleanShot.2023-08-07.at.22.20.30.mp4
folding
document symbols
suggestion: add .gitignore `*` to .next-ls dir
Elixir LS adds a
.gitignore
with an*
as the only content, which removes the need for each of us to add/.next-ls
to our.gitignore
.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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
TensorFlow
An Open Source Machine Learning Framework for Everyone
Django
The Web framework for perfectionists with deadlines.
Laravel
A PHP framework for web artisans
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.
Visualization
Some thing interesting about visualization, use data art
Game
Some thing interesting about game, make everyone happy.
Recommend Org
We are working to build community through open source technology. NB: members must have two-factor auth.
Microsoft
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba
Alibaba Open Source for everyone
D3
Data-Driven Documents codes.
Tencent
China tencent open source team.