haskell / haskell-language-server

Official haskell ide support via language server (LSP). Successor of ghcide & haskell-ide-engine.
Apache License 2.0
2.65k stars 355 forks source link

Allow disabling the formattingProvider #3283

Open andys8 opened 1 year ago

andys8 commented 1 year ago

Is your enhancement request related to a problem? Please describe.

Disabling the formattingProvider configuration does not seem possible. There is a handful of options but it can't be disabled.

The current implementation reacts formatting requests and then figures out there is plugin available. It will then result in an error

image

image

Options

Formatting provider (haskell.formattingProvider, default ormolu): what formatter to use; one of floskell, ormolu, fourmolu, stylish-haskell, or brittany (if compiled with the brittany plugin).

https://github.com/haskell/haskell-language-server/blob/aee737237c7f4542bddc7ab31a2bdd7ee0cab48d/docs/configuration.md#language-specific-server-options

Why would one want to disable the formattingProvider?

Followup of https://github.com/haskell/haskell-language-server/issues/3282

In order to configure a separate generic language server (https://github.com/iamcco/diagnostic-languageserver) to deal with formatting, Haskell language server should not "catch" the formatting requests. Instead it has to be possible to disable formatting without completely disabling the language server.

Another possibility would be - this is a bit made up - that people could have arbitrary reasons to to configure no "formattingProvider", e.g. they have configured "format on save" or they often press the format shortcut out of habit, but don't want formatting to happen temporarily for a specific or Haskell projects in general.

Describe the solution you'd like

Either "" or "none" should be handled in a way where the language server behaves as if the formatting handler wouldn't be implemented.

Maybe it is as simple as this...

when (formattingProvider config == "none") $
  pure $ Left $ ResponseError MethodNotFound "No formattingProvider configured" Nothing

Additional context

There is a test that defines how "none" is handled. It succeeds for MethodNotFound but also ""No plugin enabled for STextDocumentFormatting"" what is shown above. But this is not the wanted behavior since from the LSP client's perspective the request was handled.

https://github.com/haskell/haskell-language-server/blob/b378de2d42d1c78d0119d54e65646b475a9dc6c5/test/functional/Format.hs#L54

andys8 commented 1 year ago

Update

Rejecting requests with ResponseError MethodNotFound won't help. It will result in a different error message, but has the same effect of swallowing the format request (and second language server wouldn't handle it anymore).

image

Therefore a potential solution should be to not propagate the format server capability in the first place.

michaelpj commented 1 year ago

In order to configure a separate generic language server (https://github.com/iamcco/diagnostic-languageserver) to deal with formatting, Haskell language server should not "catch" the formatting requests.

:o what... is this? I have never seen something where people try to stitch together multiple language servers. Is that even a thing?? I'm a little unclear what the behaviour you expect is. I think it's correct for a server to respond with an error if it gets a request it can't handle.

I don't object to having a "none" option for formatting provider, though.

andys8 commented 1 year ago

:o what... is this?

Slightly offtopic but diagnostic-languageserver is quite handy: It allows you to add any linter or formatter to a generic language server implementaiton, without the hassle of installing plugins or configuring/overwriting shortcuts. Here is my config where I'm using it to format nix, dhall, shell and cabal files or run spell checks and writing recommendations.

I have never seen something where people try to stitch together multiple language servers

I'm not sure if there are many places where it would make sense to have multiple servers for a single file, but I think it's technically possible and supported by clients. In is no 1:1 mapping from filetype to server in the client configuration, but each server typically is defined with a list of file types. For example having a spell checker and linter for markdown files or commit messages, or having a more general purpose language server (e.g. yaml syntax) combined with a more specific solution (e.g. AWS CoudFormation).

I'm a little unclear what the behaviour you expect is

If I configure two servers and both can format the file, I'd say the behavior is not defined.

I think it's correct for a server to respond with an error if it gets a request it can't handle.

Correct, but ...

I don't object to having a "none" option for formatting provider, though.

There are ServerCapabilites. I haven't yet figured out where and how they're defined in HLS.

/**
 * The server provides document formatting.
 */
documentFormattingProvider?: boolean | DocumentFormattingOptions

And my assumption would be if there are two language servers defined for a file, but only one of them provides document formatting, then there is no conflict and formatting should work as expected.

Long story short

Ignoring the background, ideally if the server is configured with formattingProvder: "none" in the config, the server capabilities would be documentFormattingProvider: false, so the client knows there is no formatting handler in place and doesn't send requests (which would fail with an error).

michaelpj commented 1 year ago

There are ServerCapabilites. I haven't yet figured out where and how they're defined in HLS.

I think the lsp library automatically determines them based on whether you have a handler for the corresponding functionality. So I think it might just work out if there is no formatting handler at all.

michaelpj commented 1 year ago

And my assumption would be if there are two language servers defined for a file, but only one of them provides document formatting, then there is no conflict and formatting should work as expected.

This is the bit I don't understand. What is a "conflict"? I think there's some implicit model here where you can enable multiple servers, and then you expect them to advertise their capabilities very precisely (?) so that if you want to take an action you just look for the unique server that exposes that capability (?) and then send the request to that one (?). Or you send it to all of them, but you expect them to just ignore it if they don't handle it? I really don't know what the model is here, which makes it hard to know what to do...

andys8 commented 1 year ago

I think the lsp library automatically determines them based on whether you have a handler for the corresponding functionality

Might be the case. But it should also be possible to explicitly define them (like purescript-language-server):

https://github.com/nwolverson/purescript-language-server/blob/ff6aa80bd467abe387a6d67dacb1cba8082ffeb9/src/LanguageServer/Protocol/Setup.js#L47

Or you send it to all of them, but you expect them to just ignore it if they don't handle it?

My assumption would be that the client is smart enough to not send a formatting request to a server that states there is no formatting provider in it's capabilities.

But it's just guessing at this point. Here seems to be a good starting point to see if that's the case for CoC.

https://github.com/neoclide/coc.nvim/blob/44ed764db936ad831c5ee105773b0fbee68185e2/src/language-client/formatting.ts#L90

michaelpj commented 1 year ago

Might be the case. But it should also be possible to explicitly define the (like purescript-language-server):

I mean you certainly can, I just mean that our library setup does that.

andys8 commented 1 year ago

I did some further investigation and had some progress.

Yes, in case the language server responds initialization with ServerCapabilities and documentFormattingProvider: false the client (tested with Coc) will know there is none configured and treat it like no language server is set up for formatting. HLS wouldn't get formatting requests.

image

2022-10-13T19:00:37.400 ERROR (pid:3323017) [attach] - Error: Format provider not found for buffer: 3
    at Bb.documentFormat (/home/andreas/.vim/plugged/coc.nvim/build/index.js:264:20597)

Also if there is a second language server that does support formatting, formatting will work with the second LS and ignore the first.


Since I mentioned purescript-language-server as an example that does provide explicit server capabilities, I looked up how NoFormatter is implemented. The implementation does nothing, but also doesn't throw an error. And there is a TODO in the code that mentions "for NoFormatter don't provide formatting provider", but it's not implemented (yet).

It might be tricky (or not possible?) to get access to the configuration before the server is initialized. On first glance it doesn't seem to be as easy as grabbing the current config and setting the value to false in the capabilities. One might have to deal with dynamic registration of server capabilities to solve this instead.

michaelpj commented 1 year ago

Sorry, I think I haven't communicated it properly. HLS uses the lsp library. That handles setting the ServerCapabilities for us by looking at the handlers we provide. So we do not set them directly in HLS. (Might be wrong about this). On the other hand, I think that means that if we just don't provide a formatting handler when we set up the server, it might just work and not set the capability.

andys8 commented 1 year ago

Nope, your explanation was clear and I understood it. I would also assume that's the case 😌

I did my testing mostly with purescript-language-server since the project is easy to work with, there already exists an explicit NoFormatter configuration and explicit ServerCapabilities which makes it easy to explicitly configure the case without doubts and test how the setting behaves.

For haskell-language-server one option could be to explicitly define the capabilities but another would be to filter out formatting handlers (if the assumption is correct).

pepeiborra commented 1 year ago

This is supported already, just not in an obvious way. To disable the formatter capability, you need to disable all the plugins that carry a documentFormattingProvider.

EDIT: oh well, this won't work, as we register the request handlers unconditionally and then only run the ones that are enabled. Some work needed to fix that

chessai commented 1 year ago

Is there any update to this?

chris-martin commented 1 year ago

I'm confused about why formatting has anything to do with the responsibilities of HLS. All I ever want is for my "format document" command in vscode to do exactly the same thing that running the formatting program would do on the command line, and it never does. I'm perpetually frustrated that either because the formatter I need isn't supported, or HLS is for some reason invoking it differently. I just want HLS to back off and stop interfering on this.

michaelpj commented 1 year ago

@chris-martin please weigh in on https://github.com/haskell/haskell-language-server/issues/411

chris-martin commented 1 year ago

This plugin is what my organization recommends for setting up our formatter:

https://marketplace.visualstudio.com/items?itemName=SteefH.external-formatters

I haven't been able to use it because HLS's formatter seems to take precedence over what I need to be using.

chessai commented 1 year ago

The minimal thing I need is just to be able to set formattingProvider = null

chris-martin commented 1 year ago

Workaround for using an external fourmolu executable in VSCode to override HLS:

Install the jkillian.custom-local-formatters extension.

Add the following to .vscode/settings.json:

{
  "[haskell]": {
    "editor.defaultFormatter": "jkillian.custom-local-formatters"
  },
  "customLocalFormatters.formatters": [
    {
      "command": "fourmolu --stdin-input-file ${file}",
      "languages": ["haskell"]
    }
  ]
}
Enayaaa commented 7 months ago

I've had issues with the formatting capabilities of HLS, specifically using formattingProvider = "fourmolu" I do not understand why it did not respect my fourmolu.yaml configuration. I am a neovim user and there are some layers of software that could cause this if it's not an HLS (which I am not sure about).

I found a way to stop sending formatting requests to HLS in neovim and instead use something like null-ls(now none-ls) to use fourmolu formatter. I wanted to share the solution that worked for me for anyone else getting frustrated by it:

I added the filter argument to the vim.lsp.buf.format command:

vim.keymap.set(
  'n',
  '<space>lf',
  function()
    vim.lsp.buf.format {
      async = true,
      filter = function(client) return client.name ~= "haskell-tools.nvim" end
    }
  end,
  { noremap = true, silent = true, buffer = bufnr }
)

In my case I am using the haskell-tools.nvim plugin that registers a custom hls intance, thus the client.name ~= "haskell-tools.nvim". If you are registering hls normally you will have the client name "hls".

cipherlogs commented 5 months ago

The filter workaround is nice. but we can easily disable the formatting directly from the lsp config.

inside the hls's on_attach

set

client.server_capabilities.documentFormattingProvider = false

that's all, I do the same with tsserver, I use biome instead for formatting.

I hope it helped anyone who was trying to disable it.