diff --git a/.github/workflows/build-and-release.yaml b/.github/workflows/build-and-release.yaml index a3cb3b75..402a082f 100644 --- a/.github/workflows/build-and-release.yaml +++ b/.github/workflows/build-and-release.yaml @@ -83,6 +83,9 @@ jobs: run: cabal build --project-file cabal.project.release --only-dependencies - name: Build executables run: cabal build exe:jbeam-edit --project-file cabal.project.release + - name: Build LSP test server + run: | + cabal install exe:jbeam-lsp-test-server --project-file cabal.project.release || true - name: Run tests (GHC ${{ matrix.ghc }}) if: "!startsWith(github.ref, 'refs/tags/')" run: cabal test --project-file cabal.project.release diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 18233b7e..0050c135 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -168,5 +168,8 @@ jobs: ${{ runner.os }}-cabal-${{ matrix.ghc }}- - name: Build project (GHC ${{ steps.setup-ghc.outputs.ghc-version }}) run: cabal build --project-file cabal.project.ci all + - name: Build LSP test server + run: | + cabal install exe:jbeam-lsp-test-server --project-file cabal.project.ci || true - name: Run tests (GHC ${{ steps.setup-ghc.outputs.ghc-version }}) run: cabal test --project-file cabal.project.ci diff --git a/.github/workflows/future-proofing.yaml b/.github/workflows/future-proofing.yaml index d142a3f7..880375c1 100644 --- a/.github/workflows/future-proofing.yaml +++ b/.github/workflows/future-proofing.yaml @@ -52,5 +52,8 @@ jobs: ${{ runner.os }}-cabal-${{ steps.get-ghc.outputs.ghc-version }}- - name: Build project (GHC latest) run: cabal build --project-file cabal.project.ci all + - name: Build LSP test server + continue-on-error: true + run: cabal install exe:jbeam-lsp-test-server --project-file cabal.project.ci - name: Run tests (GHC latest) run: cabal test --project-file cabal.project.ci diff --git a/app-extra/jbeam-lsp-server/.dir-locals.el b/app-extra/jbeam-lsp-server/.dir-locals.el new file mode 100644 index 00000000..16080c1e --- /dev/null +++ b/app-extra/jbeam-lsp-server/.dir-locals.el @@ -0,0 +1,4 @@ +((haskell-mode + . ((haskell-process-type . cabal-repl) + (eval . (setq-local haskell-process-args-cabal-repl + (append '("jbeam-edit:exe:jbeam-lsp-server" "--project-file" "cabal.project.dev") haskell-process-args-cabal-repl)))))) diff --git a/app-extra/jbeam-lsp-server/Main.hs b/app-extra/jbeam-lsp-server/Main.hs new file mode 100644 index 00000000..ff6d0953 --- /dev/null +++ b/app-extra/jbeam-lsp-server/Main.hs @@ -0,0 +1,12 @@ +module Main ( + main, +) where + +import Server (runServer) + +import Formatting.Config qualified as FmtCfg + +main :: IO () +main = do + rs <- FmtCfg.readFormattingConfig + void (runServer rs) diff --git a/cabal.project b/cabal.project index de9ea8b2..185175cb 100644 --- a/cabal.project +++ b/cabal.project @@ -4,4 +4,4 @@ tests: False package jbeam-edit tests: False - flags: -dump-ast -transformation -windows-example-paths + flags: -dump-ast -transformation -lsp-server -windows-example-paths diff --git a/cabal.project.ci b/cabal.project.ci index 76c99138..83d9959e 100644 --- a/cabal.project.ci +++ b/cabal.project.ci @@ -6,4 +6,4 @@ program-options package jbeam-edit tests: True - flags: +dump-ast +transformation -windows-example-paths + flags: +dump-ast +transformation +lsp-server -windows-example-paths diff --git a/cabal.project.dev b/cabal.project.dev index 030d0de6..3a6cba13 100644 --- a/cabal.project.dev +++ b/cabal.project.dev @@ -11,4 +11,4 @@ package jbeam-edit tests: True haddock-executables: True haddock-internal: True - flags: +dump-ast +transformation -windows-example-paths + flags: +dump-ast +transformation +lsp-server -windows-example-paths diff --git a/hie.yaml b/hie.yaml index 7d406149..065dc2ee 100644 --- a/hie.yaml +++ b/hie.yaml @@ -14,8 +14,14 @@ cradle: component: lib:jbeam-edit - path: ./src-extra/transformation component: jbeam-edit:lib:jbeam-edit-transformation + - path: ./src-extra/language-server + component: jbeam-edit:lib:jbeam-language-server - path: ./app component: exe:jbeam-edit + - path: ./app-extra/jbeam-lsp-server + component: jbeam-edit:exe:jbeam-lsp-server + - path: ./app-extra/jbeam-lsp-test-server + component: jbeam-edit:exe:jbeam-lsp-test-server - path: ./tools/dump_ast component: exe:jbeam-edit-dump-ast - path: ./test diff --git a/jbeam-edit.cabal b/jbeam-edit.cabal index f7237409..c33228b2 100644 --- a/jbeam-edit.cabal +++ b/jbeam-edit.cabal @@ -49,6 +49,11 @@ flag dump-ast default: False manual: True +flag lsp-server + description: Enable LSP server (experimental) + default: False + manual: True + flag transformation description: Enable transformation (experimental) default: False @@ -152,6 +157,46 @@ library jbeam-edit-transformation else buildable: False +library jbeam-language-server + exposed-modules: + Handlers.Formatting + Server + Services.DocumentStore + + hs-source-dirs: src-extra/language-server + other-modules: Paths_jbeam_edit + autogen-modules: Paths_jbeam_edit + default-language: Haskell2010 + default-extensions: OverloadedStrings ImportQualifiedPost + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wmissing-export-lists + -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints + + build-depends: + base >=4.17 && <5, + bytestring >=0.12, + containers >=0.6, + directory >=1.3, + filepath >=1.4, + jbeam-edit, + megaparsec >=9.6, + relude >=1.2, + scientific >=0.3, + text >=2.1, + vector >=0.13 + + mixins: + base hiding (Prelude), + relude (Relude as Prelude), + relude + + if (flag(lsp-server) && impl(ghc >=9.6.6)) + build-depends: lsp >=2.7 + + else + buildable: False + executable jbeam-edit main-is: Main.hs hs-source-dirs: app @@ -232,6 +277,80 @@ executable jbeam-edit-dump-ast else buildable: False +executable jbeam-lsp-server + main-is: Main.hs + hs-source-dirs: app-extra/jbeam-lsp-server + other-modules: Paths_jbeam_edit + autogen-modules: Paths_jbeam_edit + default-language: Haskell2010 + default-extensions: OverloadedStrings ImportQualifiedPost + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wmissing-export-lists + -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints + -threaded -rtsopts -with-rtsopts=-N + + build-depends: + base >=4.17 && <5, + bytestring >=0.12, + containers >=0.6, + directory >=1.3, + filepath >=1.4, + jbeam-edit, + megaparsec >=9.6, + relude >=1.2, + scientific >=0.3, + text >=2.1, + vector >=0.13 + + mixins: + base hiding (Prelude), + relude (Relude as Prelude), + relude + + if (flag(lsp-server) && impl(ghc >=9.6.6)) + build-depends: jbeam-language-server + + else + buildable: False + +executable jbeam-lsp-test-server + main-is: Main.hs + hs-source-dirs: tools/lsp-test-server + other-modules: Paths_jbeam_edit + autogen-modules: Paths_jbeam_edit + default-language: Haskell2010 + default-extensions: OverloadedStrings ImportQualifiedPost + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wmissing-export-lists + -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints + -threaded -rtsopts -with-rtsopts=-N + + build-depends: + base >=4.17 && <5, + bytestring >=0.12, + containers >=0.6, + directory >=1.3, + filepath >=1.4, + jbeam-edit, + megaparsec >=9.6, + relude >=1.2, + scientific >=0.3, + text >=2.1, + vector >=0.13 + + mixins: + base hiding (Prelude), + relude (Relude as Prelude), + relude + + if (flag(lsp-server) && impl(ghc >=9.6.6)) + build-depends: jbeam-language-server + + else + buildable: False + test-suite jbeam-edit-test type: exitcode-stdio-1.0 main-is: Spec.hs @@ -247,6 +366,7 @@ test-suite jbeam-edit-test Parsing.JbeamSpec SpecHelper TransformationSpec + WorkspaceLspSpec Paths_jbeam_edit autogen-modules: Paths_jbeam_edit @@ -278,6 +398,12 @@ test-suite jbeam-edit-test relude (Relude as Prelude), relude + if (flag(lsp-server) && impl(ghc >=9.6.6)) + cpp-options: -DENABLE_LSP_TESTS + build-depends: + lsp >=2.7, + lsp-test + if flag(transformation) cpp-options: -DENABLE_TRANSFORMATION_TESTS build-depends: jbeam-edit-transformation diff --git a/package.yaml b/package.yaml index fad60a82..af61ce19 100644 --- a/package.yaml +++ b/package.yaml @@ -14,14 +14,16 @@ extra-source-files: - examples/jbeam/*.jbeam - examples/formatted_jbeam/*.jbeam - category: Command Line, Jbeam, Beamng - -synopsis: - A fast and reliable command-line tool for parsing, formatting, and editing JBeam files, supporting consistent node renaming, reference updating, and JBFL-based formatting. - +synopsis: A fast and reliable command-line tool for parsing, formatting, and editing + JBeam files, supporting consistent node renaming, reference updating, and JBFL-based + formatting. description: >- - jbeam-edit is a Haskell-based CLI utility for BeamNG JBeam files. It parses complete JBeam structures, preserves comments and whitespace, formats files consistently, and can automatically rename nodes and update references. Custom formatting rules are supported via JBFL (JBeam Formatting Language). See the README for usage instructions and examples: https://github.com/webdevred/jbeam-edit#readme + jbeam-edit is a Haskell-based CLI utility for BeamNG JBeam files. It parses complete + JBeam structures, preserves comments and whitespace, formats files consistently, + and can automatically rename nodes and update references. Custom formatting rules + are supported via JBFL (JBeam Formatting Language). See the README for usage instructions + and examples: https://github.com/webdevred/jbeam-edit#readme tested-with: [GHC == 9.4.7, GHC == 9.6.6] dependencies: @@ -62,6 +64,10 @@ flags: description: Enable transformation (experimental) manual: true default: false + lsp-server: + description: Enable LSP server (experimental) + manual: true + default: false windows-example-paths: description: Use executable-relative example paths (for Windows release builds) default: false @@ -77,6 +83,15 @@ internal-libraries: buildable: false source-dirs: src-extra/transformation dependencies: [jbeam-edit] + jbeam-language-server: + when: + condition: flag(lsp-server) && impl(ghc >= 9.6.6) + then: + dependencies: [lsp>=2.7] + else: + buildable: false + source-dirs: src-extra/language-server + dependencies: [jbeam-edit] library: source-dirs: src @@ -87,6 +102,17 @@ library: - condition: os(windows) && flag(windows-example-paths) cpp-options: -DWINDOWS_EXAMPLE_PATHS +_jbeam-lsp-common: &jbeam-lsp-common + main: Main.hs + ghc-options: [-threaded, -rtsopts, -with-rtsopts=-N] + when: + condition: flag(lsp-server) && impl(ghc >= 9.6.6) + then: + dependencies: [jbeam-language-server] + else: + buildable: false + dependencies: [jbeam-edit] + executables: jbeam-edit: main: Main.hs @@ -99,6 +125,12 @@ executables: cpp-options: -DENABLE_TRANSFORMATION - condition: os(windows) cpp-options: -DENABLE_WINDOWS_NEWLINES + jbeam-lsp-server: + source-dirs: app-extra/jbeam-lsp-server + <<: *jbeam-lsp-common + jbeam-lsp-test-server: + source-dirs: tools/lsp-test-server + <<: *jbeam-lsp-common jbeam-edit-dump-ast: when: condition: (flag(dump-ast) && flag(transformation)) @@ -119,6 +151,9 @@ tests: dependencies: [jbeam-edit, hspec>=2.11, hspec-megaparsec>=2.2] build-tools: [hspec-discover] when: - condition: flag(transformation) - dependencies: [jbeam-edit-transformation] - cpp-options: -DENABLE_TRANSFORMATION_TESTS + - condition: flag(lsp-server) && impl(ghc >= 9.6.6) + dependencies: [lsp-test, lsp>=2.7] + cpp-options: -DENABLE_LSP_TESTS + - condition: flag(transformation) + dependencies: [jbeam-edit-transformation] + cpp-options: -DENABLE_TRANSFORMATION_TESTS diff --git a/src-extra/language-server/.dir-locals.el b/src-extra/language-server/.dir-locals.el new file mode 100644 index 00000000..4ea4e65a --- /dev/null +++ b/src-extra/language-server/.dir-locals.el @@ -0,0 +1,4 @@ +((haskell-mode + . ((haskell-process-type . cabal-repl) + (eval . (setq-local haskell-process-args-cabal-repl + (append '("jbeam-edit:lib:jbeam-language-server" "--project-file" "cabal.project.dev") haskell-process-args-cabal-repl)))))) diff --git a/src-extra/language-server/Handlers/Formatting.hs b/src-extra/language-server/Handlers/Formatting.hs new file mode 100644 index 00000000..db341a0a --- /dev/null +++ b/src-extra/language-server/Handlers/Formatting.hs @@ -0,0 +1,97 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeOperators #-} + +module Handlers.Formatting (handlers) where + +import Core.Node (Node) +import Formatting.Rules (RuleSet) +import IOUtils + +import Data.Text qualified as T +import Formatting qualified as Fmt +import Language.LSP.Protocol.Message qualified as Msg +import Language.LSP.Protocol.Types qualified as J ( + DocumentFormattingParams (..), + Null (..), + Position (..), + Range (..), + TextDocumentIdentifier (..), + TextEdit (..), + type (|?) (..), + ) +import Language.LSP.Server qualified as S +import Parsing.Jbeam qualified as JbeamP +import Services.DocumentStore qualified as Docs + +putErrorLine' :: MonadIO m => Text -> m () +putErrorLine' = liftIO . putErrorLine + +handlers :: RuleSet -> S.Handlers (S.LspM config) +handlers rs = + S.requestHandler Msg.SMethod_TextDocumentFormatting formattingHandler + where + formattingHandler + :: Msg.TRequestMessage Msg.Method_TextDocumentFormatting + -> ( Either + (Msg.TResponseError Msg.Method_TextDocumentFormatting) + (Msg.MessageResult Msg.Method_TextDocumentFormatting) + -> S.LspM config () + ) + -> S.LspM config () + formattingHandler req responder = do + putErrorLine' "DEBUG: formattingHandler invoked" + let Msg.TRequestMessage _ _ _ (params :: J.DocumentFormattingParams) = req + handleParams rs params responder + +handleParams + :: RuleSet + -> J.DocumentFormattingParams + -> ( Either + (Msg.TResponseError Msg.Method_TextDocumentFormatting) + (Msg.MessageResult Msg.Method_TextDocumentFormatting) + -> S.LspM config () + ) + -> S.LspM config () +handleParams rs params responder = do + let J.DocumentFormattingParams {J._textDocument = textDocId} = params + J.TextDocumentIdentifier {J._uri = uri} = textDocId + mText <- liftIO $ Docs.get uri + case mText of + Nothing -> do + putErrorLine' ("DEBUG: no document in store for " <> show uri) + responder (Right (J.InR J.Null)) + Just txt -> + case JbeamP.parseNodes (encodeUtf8 txt) of + Left err -> do + liftIO . putErrorLine' $ "Parse error: " <> show err + responder (Right (J.InR J.Null)) + Right node -> runFormatNode responder rs txt node + +runFormatNode + :: (Either a ([J.TextEdit] J.|? J.Null) -> t) + -> RuleSet + -> Text + -> Node + -> t +runFormatNode responder ruleSet txt node = + let newText = Fmt.formatNode ruleSet node + edit = J.TextEdit {J._range = wholeRange txt, J._newText = newText} + in if newText == txt + then + responder (Right (J.InR J.Null)) + else + responder (Right (J.InL [edit])) + +wholeRange :: Text -> J.Range +wholeRange txt = + let ls = lines txt + numLines = max 1 (length ls) + lastLineLen = + case reverse ls of + [] -> 0 + (lastLine : _) -> T.length lastLine + in J.Range + (J.Position 0 0) + (J.Position (fromIntegral numLines) (fromIntegral lastLineLen)) diff --git a/src-extra/language-server/Server.hs b/src-extra/language-server/Server.hs new file mode 100644 index 00000000..c165127f --- /dev/null +++ b/src-extra/language-server/Server.hs @@ -0,0 +1,112 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE PolyKinds #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeOperators #-} + +module Server (runServer) where + +import Formatting.Rules (RuleSet) +import IOUtils + +import Handlers.Formatting qualified as Formatting +import Language.LSP.Protocol.Message qualified as Msg +import Language.LSP.Protocol.Types qualified as J ( + DidChangeTextDocumentParams (..), + DidCloseTextDocumentParams (..), + DidOpenTextDocumentParams (..), + TextDocumentContentChangeEvent (..), + TextDocumentContentChangePartial (..), + TextDocumentContentChangeWholeDocument (..), + TextDocumentIdentifier (..), + TextDocumentItem (..), + TextDocumentSyncKind (..), + TextDocumentSyncOptions (..), + VersionedTextDocumentIdentifier (..), + type (|?) (..), + ) +import Language.LSP.Server qualified as S +import Services.DocumentStore qualified as Docs + +staticHandlers :: RuleSet -> S.Handlers (S.LspM config) +staticHandlers rs = + mconcat + [ S.notificationHandler Msg.SMethod_Initialized $ \_notif -> + liftIO $ putErrorLine "Client initialized" + , S.notificationHandler Msg.SMethod_WorkspaceDidChangeConfiguration $ \_notif -> + liftIO $ putErrorLine "Configuration changed" + , S.notificationHandler Msg.SMethod_TextDocumentDidOpen handleDidOpen + , S.notificationHandler Msg.SMethod_TextDocumentDidClose handleDidClose + , S.notificationHandler Msg.SMethod_TextDocumentDidChange handleDidChange + ] + <> Formatting.handlers rs + +-- | Starta LSP-servern +runServer :: RuleSet -> IO Int +runServer rs = + S.runServer $ + S.ServerDefinition + { configSection = "jbeam-lsp" + , parseConfig = \_ _ -> Right () + , onConfigChange = const >> pure $ pass + , defaultConfig = () + , doInitialize = \env _req -> pure (Right env) + , staticHandlers = const $ staticHandlers rs + , interpretHandler = \env -> S.Iso (S.runLspT env) liftIO + , options = + S.defaultOptions + { S.optTextDocumentSync = + Just + ( J.TextDocumentSyncOptions + { J._openClose = Just True + , J._change = Just J.TextDocumentSyncKind_Full + , J._willSave = Nothing + , J._willSaveWaitUntil = Nothing + , J._save = Nothing + } + ) + } + } + +-- | didOpen: save docuement in DocumentStore +handleDidOpen + :: forall + {f :: Msg.MessageDirection} + {m1 :: Msg.Method f Msg.Notification} + {m2 :: Type -> Type} + . (MonadIO m2, Msg.MessageParams m1 ~ J.DidOpenTextDocumentParams) + => Msg.TNotificationMessage m1 -> m2 () +handleDidOpen (Msg.TNotificationMessage _ _ (J.DidOpenTextDocumentParams textDoc)) = + let J.TextDocumentItem {J._uri = uri, J._text = txt} = textDoc + in liftIO $ Docs.open uri txt + +-- | didChange: update document in DocumentStore +handleDidChange + :: forall + {f :: Msg.MessageDirection} + {m1 :: Msg.Method f Msg.Notification} + {m2 :: Type -> Type} + . ( MonadIO m2 + , Msg.MessageParams m1 ~ J.DidChangeTextDocumentParams + ) + => Msg.TNotificationMessage m1 -> m2 () +handleDidChange (Msg.TNotificationMessage _ _ (J.DidChangeTextDocumentParams docId changes)) = + let J.VersionedTextDocumentIdentifier {_uri = uri} = docId + in case changes of + (J.TextDocumentContentChangeEvent change : _) -> + case change of + J.InL (J.TextDocumentContentChangePartial {J._text = txt}) -> liftIO $ Docs.update uri txt + J.InR (J.TextDocumentContentChangeWholeDocument txt) -> liftIO $ Docs.update uri txt + _ -> pass + +handleDidClose + :: forall + {f :: Msg.MessageDirection} + {m1 :: Msg.Method f Msg.Notification} + {m2 :: Type -> Type} + . ( MonadIO m2 + , Msg.MessageParams m1 ~ J.DidCloseTextDocumentParams + ) + => Msg.TNotificationMessage m1 -> m2 () +handleDidClose (Msg.TNotificationMessage _ _ (J.DidCloseTextDocumentParams docId)) = + let J.TextDocumentIdentifier {_uri = uri} = docId in liftIO (Docs.delete uri) diff --git a/src-extra/language-server/Services/DocumentStore.hs b/src-extra/language-server/Services/DocumentStore.hs new file mode 100644 index 00000000..caf7dc23 --- /dev/null +++ b/src-extra/language-server/Services/DocumentStore.hs @@ -0,0 +1,30 @@ +module Services.DocumentStore (open, update, get, delete) where + +import Control.Concurrent.MVar hiding (newMVar, readMVar) +import IOUtils +import Language.LSP.Protocol.Types (Uri) +import System.IO.Unsafe (unsafePerformIO) +import Prelude hiding (get) + +import Data.Map.Strict qualified as M +import Data.Text qualified as T + +type DocumentStore = MVar (M.Map Uri T.Text) + +{-# NOINLINE store #-} +store :: DocumentStore +store = unsafePerformIO $ do + putErrorLine "[Info] Initializing DocumentStore" + newMVar M.empty + +open :: Uri -> T.Text -> IO () +open uri text = modifyMVar_ store (pure . M.insert uri text) + +update :: Uri -> T.Text -> IO () +update = open + +get :: Uri -> IO (Maybe T.Text) +get uri = M.lookup uri <$> readMVar store + +delete :: Uri -> IO () +delete uri = modifyMVar_ store (pure . M.delete uri) diff --git a/test/WorkspaceLspSpec.hs b/test/WorkspaceLspSpec.hs new file mode 100644 index 00000000..b477bf5a --- /dev/null +++ b/test/WorkspaceLspSpec.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE CPP #-} + +module WorkspaceLspSpec (spec) where + +import Test.Hspec + +#ifdef ENABLE_LSP_TESTS +import Language.LSP.Test +import Language.LSP.Protocol.Types as LSP +import qualified Data.Text.IO as T +import System.FilePath (()) + +spec :: Spec +spec = describe "JBeam LSP Formatter" $ do + it "formats a single JBeam file with a single JBFL rule correctly" $ do + runSession ("jbeam-lsp-test-server " <> ("examples" "ast" "jbfl" "minimal.hs")) fullLatestClientCaps "examples" $ do + let + jbeamFile = "jbeam" "fender.jbeam" + expectedFile = "examples" "formatted_jbeam" "fender-minimal-jbfl.jbeam" + + doc <- openDoc jbeamFile "jbeam" + + formatDoc doc (LSP.FormattingOptions 0 False Nothing Nothing Nothing) + + formatted <- documentContents doc + expected <- liftIO $ T.readFile expectedFile + + liftIO $ formatted `shouldBe` expected + +#else +spec :: Spec +spec = pass +#endif diff --git a/tools/lsp-test-server/.dir-locals.el b/tools/lsp-test-server/.dir-locals.el new file mode 100644 index 00000000..e620035b --- /dev/null +++ b/tools/lsp-test-server/.dir-locals.el @@ -0,0 +1,4 @@ +((haskell-mode + . ((haskell-process-type . cabal-repl) + (eval . (setq-local haskell-process-args-cabal-repl + (append '("jbeam-edit:lib:jbeam-lsp-test-server" "--project-file" "cabal.project.dev") haskell-process-args-cabal-repl)))))) diff --git a/tools/lsp-test-server/Main.hs b/tools/lsp-test-server/Main.hs new file mode 100644 index 00000000..b414102f --- /dev/null +++ b/tools/lsp-test-server/Main.hs @@ -0,0 +1,18 @@ +module Main (main) where + +import Relude.Unsafe (read) +import Server (runServer) + +import System.IO qualified as IO (readFile) + +main :: IO () +main = do + args <- getArgs + jbflPath <- case args of + (p : _) -> pure p + [] -> do + putStrLn "Usage: test-server " + exitFailure + + ruleSet <- read <$> IO.readFile jbflPath + void $ runServer ruleSet