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

HLS not working for file without .hs extension #3567

Open voidus opened 1 year ago

voidus commented 1 year ago

Heya,

I've searched a bit but couldn't find this mentioned before, but I found it pretty hard to search for terms like "extension", ".hs", etc., so please forgive me if this has already been brought up.

I'm using haskell to write a ./go script. In a nutshell, it fills the same role as makefiles in projects that don't use it's only-rebuild-what's-neccessary functionality, or npm's package.json scripts stuff.

I wanted to call it literally do, but then I realized that HSL doesn't work. I don't have a cabal file or anything since this is not even a haskell project, I'm using a nix devshell to get ghc with the packages I need.

When I rename the file to do.hs, everything works as it should.

While my use case might be better solved by a simpler approach anyways, this should work, right? Looking into :LspLog, it seems like HLS is running but doesn't care about the file. It didn't even print anything about hie-bios or sth.

Your environment

nvim + lspconfig haskell-language-server version: 1.10.0.0 (GHC: 9.2.7) (PATH: /nix/store/cqcm8dqw7zjj363q8z0mphg17bnxrrn1-haskell-language-server-1.10.0.0/bin/haskell-language-server-wrapper)

Which OS do you use? Arch linux, but everything is done via nix

I tried adding a hie.yaml with different content but nothing changed

If this is actually debugging-worthy, I'd be happy to trim down my stuff and upload it somewhere, reproducing should be straightforward with nix.

fendor commented 1 year ago

Hi, thank you for your bug report!

This issue is twofold, I think. First, the client needs to realise this is a haskell script, launch HLS and feed it with the file contents. I don't know whether your LSP client does that, e.g. on VSCode does not send a file without an .hs or .lhs extension to HLS. Even if it worked, HLS tries to be a multi-language Language Server, for .cabal and .hs files. It tells the difference by looking at the file extensions. If HLS accepted source files with no extension, I don't think it would be able to tell them apart easily. I don't know how HLS can support .cabal and .hs files but still satisfy your requirements.

A quick workaround might that you create a symbolic link, e.g. do.hs that points to do. Maybe that's enough to trick HLS to work correctly? Since you are nix for developing, the nix develop could create that symbolic link automatically?

voidus commented 1 year ago

The multi-filetype thing sounds about right. Editing through a symlink works indeed. It even works if the file is called .do.hs :joy:

I think I have wired up nvim to send stuff to HLS if the filetype is marked haskell. I don't know the lsp protocol, but shouldn't HLS get that info from vim?

If not, I'd propose a way to force that filetype, like # hls: haskell after the shebang. But I'd also understand if this is not a priority.

fendor commented 1 year ago

I think I have wired up nvim to send stuff to HLS if the filetype is marked haskell. I don't know the lsp protocol, but shouldn't HLS get that info from vim?

That looks possible https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem seems to have languageId. We might use that as the secondary information to distinguish the filetype.

voidus commented 1 year ago

That would be pretty cool. I'd be curious to contribute on that, but between the learning and time I'll need, I can't tell if or when I'd get around to that.

fendor commented 1 year ago

As a warning, I am not confident that works, e.g. do clients actually send "haskell" or something like that?

But if you want to give it a try, I think it should be a matter of modifying pluginResponsible to take an additional parameter for the languageId and adding a field to PluginDescriptor to add the languageId it accepts. Then, the type errors should be enough to guide you. Every time you invoke pluginResponsible, you need to pass in the languageId field, which you can obtain from TextDocument.

voidus commented 1 year ago

Just did a small experiment with Debug.trace nvim lspconfig does indeed send either haskell, cabal or lhaskell.

This should be verified with other clients, but I'm optimistic.

The bad news is that the code needs nontrivial changes. As far as I can tell from a quick look, we're actually matching the plugins on every request. But the language ID is only sent when the document is openened, so we'd need to store it somewhere.

fendor commented 1 year ago

Storing should be possible, but I can't tell you right away what changes that requires :sweat_smile:. I'll try to take a look.

voidus commented 1 year ago

I've added a new Var to the ide state and I think I can get it threaded through. https://github.com/voidus/haskell-language-server/pull/1 (I opened the PR for easier feedback, but I didn't want to increase the noise here since it's still a very early draft)

I haven't really understood the test strategy yet, maybe I'll look into that when I find some more time.

From what I see, I get the impression that we can get away with carrying around very little state outside of the shake rules, which is great of course, but maybe this change would be time to refactor that a little?

Currently, we store whether files are modified and whether it's a file of interest. Fixing this would add the language ID. Should we add a record to group this buffer information?

The bigger issue though is that with this change, we now need part of the global state (the language for the given file) to determine which plugin to run. Currently, we take the uri from the request. This seems like a significant complication for that part of the system.

I'd be happy to hear your thoughts on this, and I'll keep looking into this whenever I find some time. I will need support though, and I am conscious of taking your time.

All in all, I don't think this feature is the biggest, most important thing. For me personally, it's at least partly an excuse to dig into a haskell codebase. If you don't have time for this (or just need a while to respond) that is completely okay with me.

Edit: It's interesting though that the params aren't needed anymore for deciding if a plugin should handle a request. Maybe this could be moved to startup? Can plugins be dynamically configured?