Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Configuration file support #338

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 68 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,73 @@ Erlang:

You may want to install Elixir and Erlang from source, using the [kiex](https://github.com/taylor/kiex) and [kerl](https://github.com/kerl/kerl) tools. This will let you go-to-definition for core Elixir and Erlang modules.

## Configuration

ElixirLS can be configured through configuration/settings files

Configuration file location and precedence:

- $XDG_CONFIG_HOME/elixir_ls/config.json (e.g. `~/.config/elixir_ls/config.json`)
- repo/.elixir_ls_config.json
- Or should this be the projectDir/.elixir_ls_config.json (which is a little tricky because the project directory can be set in `workspace/didChangeConfiguration`)
- I think this should be added in the future
- Editor config (e.g. `workspace/didChangeConfiguration`)
- Note: Mainly supported via vscode-elixir-ls

### Configuration File Contents

The configuration file can include comments (e.g. lines starting with `//` are ignored)

Configuration keys:
- `dialyzerEnabled`: Run ElixirLS's rapid Dialyzer when code is saved
- Allowed values: `true`, `false`
- default: `true`
- `dialyzerWarnOpts`: Dialyzer options to enable or disable warnings. See
Dialyzer's documentation for options. Note that the `race_conditions` option
is unsupported
- Allowed values: "error_handling", "no_behaviours", "no_contracts",
"no_fail_call", "no_fun_app", "no_improper_lists", "no_match",
"no_missing_calls", "no_opaque", "no_return", "no_undefined_callbacks",
"no_unused", "underspecs", "unknown", "unmatched_returns", "overspecs",
"specdiffs"
- default: `[]`
- `dialyzerFormat`: Formatter to use for Dialyzer warnings
- Allowed values: ["dialyzer", "dialyxir_short", "dialyxir_long"]
- default: `"dialyxir_long"`
- `mixEnv`: Mix environment to use for compilation
- default: `"test"`
- `projectDir`: Subdirectory containing Mix project if not in the project root
- default: `"."`
- `fetchDeps`: Automatically fetch project dependencies when compiling
- Allowed values: `true`, `false`
- default: `true`
- `suggestSpecs`: Suggest @spec annotations inline using Dialyzer's inferred
success typings (Requires Dialyzer)
- Allowed values: `true`, `false`
- default: `true`

Example config:
```jsonc
{
// You may want to disable dialyzer if you find it unhelpful or your machine is underpowered
"dialyzerEnabled": false,

// You may want to disable fetching dependencies since it sometimes gets stuck/has race conditions
"fetchDeps": false
}
```

### Local setup

Because ElixirLS may get launched from an IDE that itself got launched from a
graphical shell, the environment may not be complete enough to run or even find
the correct Elixir/OTP version. The wrapper scripts try to configure `asdf-vm`
if available, but that may not be what you want or need. Therefore, prior to
executing Elixir, the script will source `$XDG_CONFIG_HOME/elixir_ls/setup.sh`
(e.g. `~/.config/elixir_ls/setup.sh`), if available. The environment variable
`ELS_MODE` is set to either `debugger` or `language_server` to help you decide
what to do inside the script, if needed.

## Debugger support

ElixirLS includes debugger support adhering to the [VS Code debugger protocol](https://code.visualstudio.com/docs/extensionAPI/api-debugging) which is closely related to the Language Server Protocol. At the moment, only line breakpoints are supported.
Expand Down Expand Up @@ -191,7 +258,7 @@ If you get an error like the following immediately on startup:
** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
```

and you installed Elixir and Erlang from the Erlang Solutions repository, you may not have a full installation of erlang. This can be solved with `sudo apt-get install esl-erlang`. Originally reported in [#208](https://github.com/elixir-lsp/elixir-ls/issues/208).
and you installed Elixir and Erlang from the Erlang Solutions repository, you may not have a full installation of erlang. This can be solved with `sudo apt-get install esl-erlang`. Originally reported in [#208](https://github.com/elixir-lsp/elixir-ls/issues/208). If you're running Fedora [you may need to run](https://github.com/JakeBecker/vscode-elixir-ls/issues/104#issuecomment-622414197) `sudo dnf install erlang`

## Known Issues/Limitations

Expand All @@ -210,14 +277,6 @@ Run `mix compile`, then `mix elixir_ls.release -o <release_dir>`. This builds th

If you're packaging these archives in an IDE plugin, make sure to build using the minimum supported OTP version for the best backwards-compatibility. Alternatively, you can use a [precompiled release](https://github.com/elixir-lsp/elixir-ls/releases).

### Local setup

Because ElixirLS may get launched from an IDE that itself got launched from a graphical shell, the environment may not
be complete enough to run or even find the correct Elixir/OTP version. The wrapper scripts try to configure `asdf-vm`
if available, but that may not be what you want or need. Therefore, prior to executing Elixir, the script will source
`$XDG_CONFIG_HOME/elixir_ls/setup.sh` (e.g. `~/.config/elixir_ls/setup.sh`), if available. The environment variable
`ELS_MODE` is set to either `debugger` or `language_server` to help you decide what to do inside the script, if needed.

## Acknowledgements and related projects

ElixirLS isn't the first frontend-independent server for Elixir language support. The original was [Alchemist Server](https://github.com/tonini/alchemist-server/), which powers the [Alchemist](https://github.com/tonini/alchemist.el) plugin for Emacs. Another project, [Elixir Sense](https://github.com/msaraiva/elixir_sense), builds upon Alchemist and powers the [Elixir plugin for Atom](https://github.com/msaraiva/atom-elixir) as well as another VS Code plugin, [VSCode Elixir](https://github.com/fr1zle/vscode-elixir). ElixirLS uses Elixir Sense for several code insight features. Credit for those projects goes to their respective authors.
Expand Down
8 changes: 8 additions & 0 deletions apps/elixir_ls_utils/lib/config/setting_def.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule ElixirLS.Utils.Config.SettingDef do
@moduledoc """
Defines attributes for an individual setting supported by ElixirLS.
"""

@enforce_keys [:key, :json_key, :type, :default, :doc]
defstruct [:key, :json_key, :type, :default, :doc]
end
171 changes: 171 additions & 0 deletions apps/elixir_ls_utils/lib/config_parser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
defmodule ElixirLS.Utils.ConfigParser do
@moduledoc """
Parses and loads an ElixirLS configuration file
"""

alias ElixirLS.Utils.Config.SettingDef

@settings [
%SettingDef{
key: :dialyzer_enabled,
json_key: "dialyzerEnabled",
type: :boolean,
default: true,
doc: "Run ElixirLS's rapid Dialyzer when code is saved"
},
%SettingDef{
key: :dialyzer_format,
json_key: "dialyzerFormat",
type: {:one_of, ["dialyzer", "dialyxir_short", "dialyxir_long"]},
default: "dialyxir_long",
doc: "Formatter to use for Dialyzer warnings"
},
%SettingDef{
key: :dialyzer_warn_opts,
json_key: "dialyzerWarnOpts",
type:
{:custom, ElixirLS.Utils.NimbleListChecker, :list,
[
"error_handling",
"no_behaviours",
"no_contracts",
"no_fail_call",
"no_fun_app",
"no_improper_lists",
"no_match",
"no_missing_calls",
"no_opaque",
"no_return",
"no_undefined_callbacks",
"no_unused",
"underspecs",
"unknown",
"unmatched_returns",
"overspecs",
"specdiffs"
]},
default: [],
doc:
"Dialyzer options to enable or disable warnings. See Dialyzer's documentation for options. Note that the `race_conditions` option is unsupported"
},
%SettingDef{
key: :fetch_deps,
json_key: "fetchDeps",
type: :boolean,
default: true,
doc: "Automatically fetch project dependencies when compiling"
},
%SettingDef{
key: :mix_env,
json_key: "mixEnv",
type: :string,
default: "test",
doc: "Mix environment to use for compilation"
},
%SettingDef{
key: :mix_target,
json_key: "mixTarget",
type: :string,
default: "host",
doc: "Mix target (`MIX_TARGET`) to use for compilation (requires Elixir >= 1.8)"
},
%SettingDef{
key: :project_dir,
json_key: "projectDir",
type: :string,
default: "",
doc:
"Subdirectory containing Mix project if not in the project root. " <>
"If value is \"\" then defaults to the workspace rootUri."
},
%SettingDef{
key: :suggest_specs,
json_key: "suggestSpecs",
type: :boolean,
default: true,
doc:
"Suggest @spec annotations inline using Dialyzer's inferred success typings " <>
"(Requires Dialyzer)"
},
%SettingDef{
key: :trace,
json_key: "trace",
type: :map,
default: %{},
doc: "Ignored"
}
]

def load_config_file(path) do
with {:ok, contents} <- File.read(path) do
load_config(contents)
end
end

def load_config(contents) do
with {:ok, settings_map} <- json_decode(contents),
{:ok, validated_options} <- parse_config(settings_map) do
{:ok, Map.new(validated_options), []}
end
end

def default_config do
@settings
|> Map.new(fn %SettingDef{} = setting_def ->
%SettingDef{key: key, default: default} = setting_def
{key, default}
end)
end

@doc """
Parse the raw decoded JSON to the settings map (including translation from
camelCase to snake_case)
"""
def parse_config(settings_map) do
# Because we use a configuration layering approach, this configuration
# parsing should be based on the settings_map and not the possible settings.
# The return value should be *only* the settings that were passed in, don't
# return the defaults here.
values =
settings_map
|> Enum.map(fn {json_key, value} ->
case translate_key(json_key) do
{:ok, key} -> {:ok, {key, value}}
{:error, "unknown key"} -> {:error, {:unrecognized_configuration_key, json_key, value}}
end
end)

{good, errors} = Enum.split_with(values, &match?({:ok, _}, &1))
config = Map.new(good, fn {:ok, {key, val}} -> {key, val} end)

{:ok, config, errors}
end

for %SettingDef{key: key, json_key: json_key} <- @settings do
defp translate_key(unquote(json_key)) do
{:ok, unquote(key)}
end
end

defp translate_key(_), do: {:error, "unknown key"}

for setting <- @settings do
def valid_key?(unquote(setting.json_key)), do: true
end

def valid_key?(_), do: false

def json_decode(contents) when is_binary(contents) do
contents
|> String.split(["\n", "\r", "\r\n"], trim: true)
|> Enum.map(&String.trim/1)
# Ignore json comments
|> Enum.reject(&String.starts_with?(&1, "#"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, good catch. I've obviously haven't been doing very much js development recently. However since we're not using a full processor I don't think it's feasible to support multi line comments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. A simple full line comment support is good enough.

|> Enum.join()
|> JasonVendored.decode()
|> case do
{:ok, _} = ok -> ok
{:error, %JasonVendored.DecodeError{} = err} -> {:error, {:invalid_json, err}}
end
end
end
32 changes: 32 additions & 0 deletions apps/elixir_ls_utils/lib/xdg.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule ElixirLS.Utils.XDG do
@moduledoc """
Utilities for reading files within ElixirLS's XDG configuration directory
"""

@default_xdg_directory "$HOME/.config"

def read_elixir_ls_config_file(path) do
xdg_directory()
|> Path.join("elixir_ls")
|> Path.join(path)
|> File.read()
|> case do
{:ok, file_contents} -> {:ok, file_contents}
err -> err
end
end

defp xdg_directory do
case System.get_env("XDG_CONFIG_HOME") do
nil ->
@default_xdg_directory

xdg_directory ->
if File.dir?(xdg_directory) do
xdg_directory
else
raise "$XDG_CONFIG_HOME environment variable set, but directory does not exist"
end
end
end
end
Loading