garrison

garrison

How can I prevent the LSP from invoking my custom Mix compiler?

Background

So I’ve been hacking away at a simple styled component library for Phoenix (source here if you’re curious - not done!) which uses a custom Mix compiler to compile the stylesheet.

I’d been dealing with a weird issue where the stylesheet gets recompiled multiple times (triggering multiple live reloads in the console), and I hadn’t been able to track down the root cause. I figured I would get around to making a post about it eventually.

Fast-forward to today, I was testing some config changes when I noticed that the compiler was writing a stylesheet even though it was disabled (via config). Obviously something strange was going on, so I investigated further, but after thoroughly instrumenting the code with IO.inspects (as one does) I was unable to figure out what was invoking the compiler - it wasn’t printing anything, but it was still writing the output!

At this point fully convinced my compiler was haunted, I tried killing the Phoenix server and inspecting the output stylesheet to see if it was still compiling. Which it was. Obviously I had a phantom process somewhere, so I checked the process list and, sure enough, there were a couple extra beam processes floating around.

I stared at the process list for about 10 seconds before it hit me. The LSP!

Question

Is there a canonical way to stop the LSP (elixir-ls in this case) from invoking my Mix compiler?

One way to do this which I think would work is to fingerprint the LSP calls via the args that they pass into the compiler’s run/1 function. In particular, a bit of logging reveals that the LSP calls the compiler with the --return-errors flag (and a couple other unusual flags), which I presume is uncommon in normal usage. But this feels like a hack.

Also, does anyone know if and how Surface solves this problem with their compiler? A quick look through their code/issues didn’t reveal anything pertaining to the LSP.

Most Liked

lukaszsamson

lukaszsamson

ElixirLS Core Team

ElixirLS invokes the equivalent of mix compile (Mix.Task.run("compile", opts)) whenever one of the open files gets saved or changes in the watched files get detected. Instead of unreliable tracking of options passed to the task (which can change) I’d advise you to make your compiler detect changes and recompile only when needed. You can return :noop if e.g. the hash of files did not change since last successful compilation.

garrison

garrison

I took some time to properly debug what was going on here. I first nuked my .elixir_ls directories and upgraded to the latest version (0.17.10). Coincidentally, I see you released this version while I was typing up my last post!

I have found the source of the behavior I was seeing. It’s very specific to the way I happened to be testing my code, so I’ll explain in detail.

The corp_style project is meant to be used as a library (it generates scoped css styles for heex components). The output path (and a couple other things) are configured in a manner similar to the esbuild and tailwind packages that ship with Phoenix.

The config is placed under a “profile” with an arbitrary key, though currently I only support one such key (default). It looks like this:

config :corp_style,
  default: [
    out_path: "some/path/to.css",
  ]

Then the compiler loads the config by the usual method, Application.get_env(:corp_style, :default). If the result is truthy, it compiles the CSS. If the result is nil (i.e. the library has not been configured), it does nothing.

While testing, I renamed the key from default to something else to disable the compiler. However, the compiler was still running, which is the bug that led to me opening this thread.

I now see what is happening here is that elixir-ls is, in fact, correctly reloading the config and recompiling, but the config is merged instead of overwritten on reload. So, when I rename default to something else and elixir-ls recompiles, the default key still exists, and so my compiler still runs.

I find this merging behavior very surprising. I understand that Elixir’s config deep-merges keys, but I assumed (and I think most others would assume) that when you remove a key from your config that it’s actually gone. Perhaps this is why Phoenix enforces a server restart after a config change?

Anyway, now that I know what was actually causing the behavior I was seeing I can work around it more easily. Thank you for your help!

garrison

garrison

I put together a minimal reproduction. The bug only seems to occur with a dependency (note that I only tested a path dependency, not git or hex dependencies).

Steps to reproduce:

  1. Clone these repositories into the same directory.
    https://git.sr.ht/~garrisonc/exls_bug_config_merge
    https://git.sr.ht/~garrisonc/exls_bug_config_merge_parent
    
    Note that the latter has a relative path dependency on the former
  2. Open exls_bug_config_merge_parent/config/config.exs in an LSP-enabled editor.
  3. Note that a warning message has been printed to the LSP log with a dump of the config keys for the child project. This message comes from the Mix compiler task.
  4. Change the config key in the parent’s config.exs to any other value.
  5. Note that the warning message now prints multiple keys, indicating that they have been merged.
  6. Repeat steps 4 and 5 indefinitely.

Tested in neovim on macos via nvim-lspconfig.

christhekeele

christhekeele

Additionally, IIRC ElixirLS runs with MIX_ENV=“test” by default. You could always check the value of Mix.env() in your mix.exs and not add your compiler to the def project, do: [compilers: ...] list when it is "test".

lukaszsamson

lukaszsamson

ElixirLS Core Team

However, the issue I was running into was that when I changed my config.exs to disable the compiler it was still being invoked by the LSP. I assumed at the time that the LSP was hot reloading the code similar to the Phoenix server, and so had to be restarted to re-read the config.

Config reload happens in elixir-ls/apps/language_server/lib/language_server/build.ex at 2d6c4b8784762e1c8183d0b73c0eeb1f93a6779e · elixir-lsp/elixir-ls · GitHub

For the record I did implement the --return-errors fingerprint hack, and it works fine for the time being, but I’m not happy with it either.

If you need to check it via fingerprint hacks, then setting an environment variable in ElixirLS configuration may be a better option.

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New

We're in Beta

About us Mission Statement