Skip to content

Commit fc7ac76

Browse files
committed
implement basic parsing with parseProject
1 parent 595efc1 commit fc7ac76

File tree

6 files changed

+112
-34
lines changed

6 files changed

+112
-34
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ store/
5151
gh-release-artifacts/
5252

5353
.hls/
54+
55+
# local cabal package
56+
vendor/

cabal.project

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ packages:
66
./ghcide
77
./hls-plugin-api
88
./hls-test-utils
9+
./vendor/parse-cabal-project/cabal/Cabal
10+
./vendor/parse-cabal-project/cabal/Cabal-syntax
11+
./vendor/parse-cabal-project/cabal/cabal-install
12+
./vendor/parse-cabal-project/cabal/cabal-install-solver
13+
./vendor/parse-cabal-project/cabal/Cabal-described
14+
./vendor/parse-cabal-project/cabal/Cabal-tree-diff
15+
16+
package cabal-install
17+
tests: False
18+
benchmarks: False
919

1020

1121
index-state: 2025-05-12T13:26:29Z

haskell-language-server.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ library hls-cabal-project-plugin
365365
, aeson
366366
, Cabal
367367
, pretty
368-
, cabal-install-parsers >= 0.6 && < 0.7
368+
, cabal-install
369369

370370
hs-source-dirs: plugins/hls-cabal-project-plugin/src
371371

plugins/hls-cabal-project-plugin/src/Ide/Plugin/CabalProject.hs

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Control.Monad.Trans.Class (lift)
1515
import Control.Monad.Trans.Maybe (runMaybeT)
1616
import qualified Data.ByteString as BS
1717
import Data.Hashable
18-
import Data.HashMap.Strict (HashMap)
18+
import Data.HashMap.Strict (HashMap, toList)
1919
import qualified Data.HashMap.Strict as HashMap
2020
import qualified Data.List as List
2121
import qualified Data.List.NonEmpty as NE
@@ -45,7 +45,7 @@ import Distribution.PackageDescription.Configuration (flattenPackageDe
4545
import Distribution.Parsec.Error
4646
import qualified Distribution.Parsec.Position as Syntax
4747
import GHC.Generics
48-
import Ide.Plugin.CabalProject.Parse (parseCabalProjectContents)
48+
import Ide.Plugin.CabalProject.Parse (parseCabalProjectFileContents)
4949
import Ide.Plugin.Error
5050
import Ide.Types
5151
import qualified Language.LSP.Protocol.Lens as JL
@@ -95,16 +95,14 @@ descriptor recorder plId =
9595
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri, _version}) -> liftIO $ do
9696
whenUriFile _uri $ \file -> do
9797
log' Debug $ LogDocOpened _uri
98-
result <- parseCabalProjectContents (fromNormalizedFilePath file)
99-
case result of
100-
Left err -> putStrLn $ "Cabal project parse failed: " ++ err
101-
Right project -> putStrLn $ "Cabal project parsed successfully: " ++ show project
98+
parseAndPrint (fromNormalizedFilePath file)
10299
restartCabalShakeSession (shakeExtras ide) vfs file "(opened)" $
103100
addFileOfInterest recorder ide file Modified{firstOpen = True}
104101
, mkPluginNotificationHandler LSP.SMethod_TextDocumentDidChange $
105102
\ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier{_uri} _) -> liftIO $ do
106103
whenUriFile _uri $ \file-> do
107104
log' Debug $ LogDocModified _uri
105+
parseAndPrint (fromNormalizedFilePath file)
108106
restartCabalShakeSession (shakeExtras ide) vfs file "(changed)" $
109107
addFileOfInterest recorder ide file Modified{firstOpen = False}
110108
, mkPluginNotificationHandler LSP.SMethod_TextDocumentDidSave $
@@ -130,10 +128,20 @@ descriptor recorder plId =
130128
whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
131129
whenUriFile uri act = whenJust (uriToFilePath uri) $ act . toNormalizedFilePath'
132130

133-
cabalRules :: Recorder (WithPriority Log) -> PluginId -> Rules ()
134-
cabalRules recorder _ = do
135-
ofInterestRules recorder
136-
-- cabalProjectParseRules recorder
131+
parseAndPrint :: FilePath -> IO ()
132+
parseAndPrint file = do
133+
(warnings, res) <- parseCabalProjectFileContents file
134+
135+
mapM_ (putStrLn . ("[Cabal warning] " ++) . show) warnings
136+
137+
case res of
138+
Left (_mbSpecVer, errs) ->
139+
putStrLn $
140+
"Cabal project parse failed:\n" ++ unlines (map show (NE.toList errs))
141+
142+
Right project ->
143+
putStrLn $
144+
"Cabal project parsed successfully:\n" ++ show project
137145

138146
{- | Helper function to restart the shake session, specifically for modifying .cabal files.
139147
No special logic, just group up a bunch of functions you need for the base
@@ -150,6 +158,46 @@ restartCabalShakeSession shakeExtras vfs file actionMsg actionBetweenSession = d
150158
keys <- actionBetweenSession
151159
return (toKey GetModificationTime file:keys)
152160

161+
162+
cabalRules :: Recorder (WithPriority Log) -> PluginId -> Rules ()
163+
cabalRules recorder _ = do
164+
-- Make sure we initialise the cabal files-of-interest.
165+
ofInterestRules recorder
166+
-- Rule to produce diagnostics for cabal files.
167+
define (cmapWithPrio LogShake recorder) $ \ParseCabalProjectFields file -> do
168+
config <- getPluginConfigAction plId
169+
if not (plcGlobalOn config && plcDiagnosticsOn config)
170+
then pure ([], Nothing)
171+
else do
172+
-- whenever this key is marked as dirty (e.g., when a user writes stuff to it),
173+
-- we rerun this rule because this rule *depends* on GetModificationTime.
174+
(t, mCabalSource) <- use_ GetFileContents file
175+
log' Debug $ LogModificationTime file t
176+
contents <- case mCabalSource of
177+
Just sources ->
178+
pure $ Encoding.encodeUtf8 $ Rope.toText sources
179+
Nothing -> do
180+
liftIO $ BS.readFile $ fromNormalizedFilePath file
181+
182+
case Parse.readCabalProjectFields file contents of
183+
Left _ ->
184+
pure ([], Nothing)
185+
Right fields ->
186+
pure ([], Just fields)
187+
188+
{- | This is the kick function for the cabal plugin.
189+
We run this action, whenever we shake session us run/restarted, which triggers
190+
actions to produce diagnostics for cabal files.
191+
192+
It is paramount that this kick-function can be run quickly, since it is a blocking
193+
function invocation.
194+
-}
195+
kick :: Action ()
196+
kick = do
197+
files <- HashMap.keys <$> getCabalFilesOfInterestUntracked
198+
Shake.runWithSignal (Proxy @"kick/start/cabal-project") (Proxy @"kick/done/cabal-project") files Types.ParseCabalProjectFile
199+
200+
153201
-- ----------------------------------------------------------------
154202
-- Cabal file of Interest rules and global variable
155203
-- ----------------------------------------------------------------

plugins/hls-cabal-project-plugin/src/Ide/Plugin/CabalProject/Diagnostics.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module Ide.Plugin.CabalProject.Diagnostics where
22

33
diagnostic = undefined
44

5+
-- can use renderParseError: https://github.com/haskell/cabal/blob/master/cabal-install/src/Distribution/Client/Utils/Parsec.hs
6+
57
-- {-# LANGUAGE DuplicateRecordFields #-}
68
-- {-# LANGUAGE OverloadedStrings #-}
79
-- module Ide.Plugin.CabalProject.Diagnostics
Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
11
{-# LANGUAGE OverloadedStrings #-}
22

33
module Ide.Plugin.CabalProject.Parse
4-
( parseCabalProjectContents
4+
( parseCabalProjectFileContents
55
) where
66

7-
import Data.Void (Void)
7+
-- base -----------------------------------------------------------------------
8+
import Control.Monad (unless)
9+
import qualified Data.ByteString as BS
10+
import Data.List.NonEmpty (NonEmpty (..))
11+
import Distribution.Client.HttpUtils (configureTransport)
12+
import Distribution.Client.ProjectConfig.Parsec (ProjectConfigSkeleton,
13+
parseProject)
14+
import Distribution.Client.ProjectConfig.Types (ProjectConfigToParse (..))
15+
import Distribution.Fields (PError (..),
16+
PWarning (..))
17+
import Distribution.Fields.ParseResult (ParseResult,
18+
runParseResult)
19+
import Distribution.Types.Version (Version)
20+
import Distribution.Verbosity (normal)
21+
import System.Directory (doesFileExist)
22+
import System.FilePath (takeDirectory)
823

9-
-- cabal-install-parsers 0.6 modules -----------------------------
10-
import Cabal.Parse (ParseError)
11-
import Cabal.Project (Project,
12-
parseProject)
24+
parseCabalProjectFileContents
25+
:: FilePath
26+
-> IO ([PWarning]
27+
, Either (Maybe Version, NonEmpty PError) ProjectConfigSkeleton)
28+
parseCabalProjectFileContents fp = do
29+
bytes <- BS.readFile fp
30+
let toParse = ProjectConfigToParse bytes
31+
rootDir = takeDirectory fp
32+
verb = normal
33+
httpTransport <- configureTransport verb [fp] Nothing
1334

14-
-- error type lives in Cabal-syntax
15-
-- import Distribution.Parsec.Error (ParseError)
35+
parseRes :: ParseResult ProjectConfigSkeleton
36+
<- parseProject rootDir fp httpTransport verb toParse
1637

17-
import Distribution.Types.GenericPackageDescription (GenericPackageDescription)
38+
pure (runParseResult parseRes)
1839

19-
import qualified Data.ByteString as BS
20-
-- import Distribution.Parsec.Project (parseProject)
21-
-- import Distribution.Parsec.Common (ParseError)
22-
import Data.List.NonEmpty (NonEmpty)
23-
import Data.Text (pack)
24-
25-
parseCabalProjectContents :: FilePath -> IO (Either String (Project Void String String))
26-
parseCabalProjectContents file = do
27-
contents <- BS.readFile file
28-
case parseProject file contents of
29-
Left parseErr ->
30-
pure $ Left ("Parse error in " ++ file ++ ": " ++ show parseErr)
31-
Right project ->
32-
pure $ Right project
40+
-- parseCabalProjectContents :: FilePath -> IO (Either String (Project Void String String))
41+
-- parseCabalProjectContents file = do
42+
-- contents <- BS.readFile file
43+
-- case parseProject file contents of
44+
-- Left parseErr ->
45+
-- pure $ Left ("Parse error in " ++ file ++ ": " ++ show parseErr)
46+
-- Right project ->
47+
-- pure $ Right project

0 commit comments

Comments
 (0)