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

Improving the "Let's write a Haskell Language Server Plugin" tutorial #3877

Open gfarrell opened 10 months ago

gfarrell commented 10 months ago

(This issue is WIP -- I'm going to record all the things I found confusing about writing a plugin and then suggest changes, but welcome comments as I go.)

I'm currently following the plugin writing documentation / tutorial, and encountered the following things. I'm using the plugin name ArgSwap because that's the plugin I was writing.

Creating a new plugin and linking it

I had to edit a load of files and do a load of work just to include my plugin in my build:

I then had to "link" it:

  1. add my plugin to cabal.project by adding ./plugins/hls-argswap-plugin to packages
  2. create a new flag in haskell-language-server.cabal
diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal
index c925b916..5d54dd67 100644
--- a/haskell-language-server.cabal
+++ b/haskell-language-server.cabal
@@ -164,6 +164,11 @@ flag overloadedRecordDot
   default:     True
   manual:      True

+flag argSwap
+  description: Enable the args swapping plugin
+  default:     True
+  manual:      True
+
 -- formatters

 flag floskell
@@ -306,6 +311,11 @@ common overloadedRecordDot
     build-depends: hls-overloaded-record-dot-plugin == 2.4.0.0
     cpp-options: -Dhls_overloaded_record_dot

+common argSwap
+  if flag(argSwap)
+    build-depends: hls-argswap-plugin == 2.4.0.0
+    cpp-options: -Dhls_argswap
+
 -- formatters

 common floskell
@@ -365,6 +375,7 @@ library
                   , stylishHaskell
                   , refactor
                   , overloadedRecordDot
+                  , argSwap

   exposed-modules:
     Ide.Arguments
  1. Use that flag to enable the plugin in src/HlsPlugins.hs
diff --git a/src/HlsPlugins.hs b/src/HlsPlugins.hs
index 4d371859..cb4b12b2 100644
--- a/src/HlsPlugins.hs
+++ b/src/HlsPlugins.hs
@@ -94,6 +94,10 @@ import qualified Ide.Plugin.ExplicitFields         as ExplicitFields
 import qualified Ide.Plugin.OverloadedRecordDot    as OverloadedRecordDot
 #endif

+#if hls_argswap
+import qualified Ide.Plugin.ArgSwap as ArgSwap
+#endif
+
 -- formatters

 #if hls_floskell
@@ -223,6 +227,9 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins
 #endif
 #if hls_overloaded_record_dot
       let pId = "overloaded-record-dot" in OverloadedRecordDot.descriptor (pluginRecorder pId) pId :
+#endif
+#if hls_argswap
+      let pId = "arg-swap" in ArgSwap.descriptor (pluginRecorder pId) pId :
 #endif
       GhcIde.descriptors (pluginRecorder "ghcide")
  1. add the plugin to stack.yaml and stack-lts21.yaml under packages:

Not knowing what is imported whence

Example plugins I looked at used some implicit imports. HLS wouldn't work for me on the HLS codebase, so I had to make a lot of guesses as to what came whence.

Some outdated types (and missing links)

Near the beginning of the tutorial, there is a description of the PluginDescriptor datatype as being defined in Ide.Plugin as:

data PluginDescriptor =
  PluginDescriptor { pluginId                 :: !PluginId
                   , pluginRules              :: !(Rules ())
                   , pluginCommands           :: ![PluginCommand]
                   , pluginCodeActionProvider :: !(Maybe CodeActionProvider)
                   , pluginCodeLensProvider   :: !(Maybe CodeLensProvider)
                   , pluginHoverProvider      :: !(Maybe HoverProvider)
                   , pluginSymbolsProvider    :: !(Maybe SymbolsProvider)
                   , pluginFormattingProvider :: !(Maybe (FormattingProvider IO))
                   , pluginCompletionProvider :: !(Maybe CompletionProvider)
                   , pluginRenameProvider     :: !(Maybe RenameProvider)
                   }

In fact (at least in 2.4.0) it is defined in Ide.Types as:

data PluginDescriptor (ideState :: Type) =
  PluginDescriptor { pluginId           :: !PluginId
                   -- ^ Unique identifier of the plugin.
                   , pluginPriority     :: Natural
                   -- ^ Plugin handlers are called in priority order, higher priority first
                   , pluginRules        :: !(Rules ())
                   , pluginCommands     :: ![PluginCommand ideState]
                   , pluginHandlers     :: PluginHandlers ideState
                   , pluginConfigDescriptor :: ConfigDescriptor
                   , pluginNotificationHandlers :: PluginNotificationHandlers ideState
                   , pluginModifyDynflags :: DynFlagsModifications
                   , pluginCli            :: Maybe (ParserInfo (IdeCommand ideState))
                   , pluginFileType       :: [T.Text]
                   -- ^ File extension of the files the plugin is responsible for.
                   --   The plugin is only allowed to handle files with these extensions.
                   --   When writing handlers, etc. for this plugin it can be assumed that all handled files are of this type.
                   --   The file extension must have a leading '.'.
                   }

Keeping documentation up to date is hard, but one of the things which would be really helpful (especially as HLS just won't run on the HLS codebase for me) is links to the definitions.

Alternatively, I'd love it if HLS was packaged up on Hackage and therefore Hoogleable, because at the moment building local hoogle (via stack hoogle) exhausted my machine's memory (and I have 32GB of RAM on this laptop).

fendor commented 10 months ago

Hi, thank you for your bug report! Your feedback is very welcome and we appreciate any improvements to our quite outdated tutorial!

Only addressing a subset of the issue for now:

Some outdated types (and missing links)

The PR #3655 should take care of these issues.

Alternatively, I'd love it if HLS was packaged up on Hackage and therefore Hoogleable

It is not hackage but not on stackage which is likely too much work for the resources we currently have.

Not knowing what is imported whence

This is a very painful issue with our setup which we try to improve. For now, it should work much better if you use cabal for building HLS and create a hie.yaml file in the root of your project with the contents:

cradle:
  cabal:

This instructs haskell-language-server to compile this repository using the cabal build tool.

gfarrell commented 10 months ago

@fendor thanks. I'm slowly working through trying to create my plugin (which might be beyond me). I'm happy to keep adding to this issue (and then maybe contribute to the documentation myself) if that is helpful?