packages feed

haskell-lsp 0.20.0.1 → 0.21.0.0

raw patch · 9 files changed

+239/−66 lines, 9 filesdep ~haskell-lsp-typesPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: haskell-lsp-types

API changes (from Hackage documentation)

Files

ChangeLog.md view
@@ -1,11 +1,22 @@ # Revision history for haskell-lsp +## 0.21.0.0++* Stop getCompletionPrefix from crashing if beforePos is empty+* Add DidChangeWatchedFilesRegistrationOptions+* Add NormalizedFilePath from ghcide+* Add diagnostic and completion tags+* Fix language server example+* Correctly fix the problem with '$/' notifications+* Add azure ci+ ## 0.20.0.1  * Fix Haddock generation syntax error  ## 0.20.0.0 +* Force utf8 encoding when writing vfs temp files * Don't log errors for '$/' notifications (@jinwoo) * Force utf8 encoding when writing vfs temp files (@jneira) * Store a hash in a NormalizedUri (@mpickering)
example/Main.hs view
@@ -32,7 +32,6 @@ import           Language.Haskell.LSP.VFS import           System.Exit import qualified System.Log.Logger                     as L-import qualified Data.Rope.UTF16                       as Rope   -- ---------------------------------------------------------------------@@ -64,9 +63,15 @@       liftIO $ U.logs "main.run:dp after dispatcherProc"       return Nothing +    callbacks = Core.InitializeCallbacks+      { Core.onInitialConfiguration = const $ Right ()+      , Core.onConfigurationChange = const $ Right ()+      , Core.onStartup = dp+      }+   flip E.finally finalProc $ do     Core.setupLogger (Just "/tmp/lsp-hello.log") [] L.DEBUG-    CTRL.run (return (Right ()), dp) (lspHandlers rin) lspOptions (Just "/tmp/lsp-hello-session.log")+    CTRL.run callbacks (lspHandlers rin) lspOptions (Just "/tmp/lsp-hello-session.log")    where     handlers = [ E.Handler ioExcept@@ -107,7 +112,7 @@ publishDiagnostics :: Int -> J.NormalizedUri -> J.TextDocumentVersion -> DiagnosticsBySource -> R () () publishDiagnostics maxToPublish uri v diags = do   lf <- ask-  liftIO $ (Core.publishDiagnosticsFunc lf) maxToPublish uri v diags+  liftIO $ Core.publishDiagnosticsFunc lf maxToPublish uri v diags  -- --------------------------------------------------------------------- @@ -196,11 +201,11 @@         mdoc <- liftIO $ Core.getVirtualFileFunc lf doc         case mdoc of           Just (VirtualFile _version str _) -> do-            liftIO $ U.logs $ "reactor:processing NotDidChangeTextDocument: vf got:" ++ (show $ Rope.toString str)+            liftIO $ U.logs $ "reactor:processing NotDidChangeTextDocument: vf got:" ++ show str           Nothing -> do             liftIO $ U.logs "reactor:processing NotDidChangeTextDocument: vf returned Nothing" -        liftIO $ U.logs $ "reactor:processing NotDidChangeTextDocument: uri=" ++ (show doc)+        liftIO $ U.logs $ "reactor:processing NotDidChangeTextDocument: uri=" ++ show doc        -- ------------------------------- @@ -234,7 +239,7 @@        HandlerRequest (ReqHover req) -> do         liftIO $ U.logs $ "reactor:got HoverRequest:" ++ show req-        let J.TextDocumentPositionParams _doc pos = req ^. J.params+        let J.TextDocumentPositionParams _doc pos _workDoneToken = req ^. J.params             J.Position _l _c' = pos          let@@ -255,7 +260,7 @@          let           -- makeCommand only generates commands for diagnostics whose source is us-          makeCommand (J.Diagnostic (J.Range start _) _s _c (Just "lsp-hello") _m _l) = [J.Command title cmd cmdparams]+          makeCommand (J.Diagnostic (J.Range start _) _s _c (Just "lsp-hello") _m _t _l) = [J.Command title cmd cmdparams]             where               title = "Apply LSP hello command:" <> head (T.lines _m)               -- NOTE: the cmd needs to be registered via the InitializeResponse message. See lspOptions above@@ -266,7 +271,7 @@                       , J.Object $ H.fromList [("start_pos",J.Object $ H.fromList [("position",    J.toJSON start)])]                       ]               cmdparams = Just args-          makeCommand (J.Diagnostic _r _s _c _source _m _l) = []+          makeCommand (J.Diagnostic _r _s _c _source _m _t _l) = []         let body = J.List $ map J.CACommand $ concatMap makeCommand diags             rsp = Core.makeResponseMessage req body         reactorSend $ RspCodeAction rsp@@ -318,6 +323,7 @@               Nothing  -- code               (Just "lsp-hello") -- source               "Example diagnostic message"+              Nothing -- tags               (Just (J.List []))             ]   -- reactorSend $ J.NotificationMessage "2.0" "textDocument/publishDiagnostics" (Just r)@@ -336,7 +342,7 @@  lspOptions :: Core.Options lspOptions = def { Core.textDocumentSync = Just syncOptions-                 , Core.executeCommandProvider = Just (J.ExecuteCommandOptions (J.List ["lsp-hello-command"]))+                 , Core.executeCommandCommands = Just ["lsp-hello-command"]                  }  lspHandlers :: TChan ReactorInput -> Core.Handlers
haskell-lsp.cabal view
@@ -1,5 +1,5 @@ name:                haskell-lsp-version:             0.20.0.1+version:             0.21.0.0 synopsis:            Haskell library for the Microsoft Language Server Protocol  description:         An implementation of the types, and basic message server to@@ -46,7 +46,7 @@                      , filepath                      , hslogger                      , hashable-                     , haskell-lsp-types == 0.20.*+                     , haskell-lsp-types == 0.21.*                      , lens >= 4.15.2                      , mtl                      , network-uri
src/Language/Haskell/LSP/Core.hs view
@@ -433,11 +433,13 @@           let msg = T.pack $ unwords $ ["haskell-lsp:parse error.", show json, show err] ++ _ERR_MSG_URL           sendErrorLog tvarDat msg   where-    isOptionalNotification req' =-      case (req', json) of-        (NotCustomClient _, J.String method)-          | "$/" `T.isPrefixOf` method -> True-        _ -> False+    isOptionalNotification req+      | NotCustomClient _ <- req+      , J.Object object <- json+      , Just (J.String method) <- HM.lookup "method" object+      , "$/" `T.isPrefixOf` method+      = True+      | otherwise = False  handleInitialConfig   :: (Show config)
src/Language/Haskell/LSP/VFS.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -281,9 +282,10 @@         curLine <- headMaybe $ T.lines $ Rope.toText                              $ fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine l ropetext         let beforePos = T.take c curLine-        curWord <- case T.last beforePos of-                     ' ' -> return "" -- don't count abc as the curword in 'abc '-                     _ -> lastMaybe (T.words beforePos)+        curWord <-+            if | T.null beforePos -> Just ""+               | T.last beforePos == ' ' -> Just "" -- don't count abc as the curword in 'abc '+               | otherwise -> lastMaybe (T.words beforePos)          let parts = T.split (=='.')                       $ T.takeWhileEnd (\x -> isAlphaNum x || x `elem` ("._'"::String)) curWord
test/DiagnosticsSpec.hs view
@@ -35,14 +35,14 @@     rng = J.Range (J.Position 0 1) (J.Position 3 0)     loc = J.Location (J.Uri "file") rng   in-    J.Diagnostic rng Nothing Nothing ms str (Just (J.List [J.DiagnosticRelatedInformation loc str]))+    J.Diagnostic rng Nothing Nothing ms str Nothing (Just (J.List [J.DiagnosticRelatedInformation loc str]))  mkDiagnostic2 :: Maybe J.DiagnosticSource -> Text -> J.Diagnostic mkDiagnostic2 ms str =   let     rng = J.Range (J.Position 4 1) (J.Position 5 0)     loc = J.Location (J.Uri "file") rng-  in J.Diagnostic rng Nothing Nothing ms str (Just (J.List [J.DiagnosticRelatedInformation loc str]))+  in J.Diagnostic rng Nothing Nothing ms str Nothing (Just (J.List [J.DiagnosticRelatedInformation loc str]))  -- --------------------------------------------------------------------- 
test/JsonSpec.hs view
@@ -36,6 +36,7 @@     prop "MarkupContent"     (propertyJsonRoundtrip :: MarkupContent -> Property)     prop "HoverContents"     (propertyJsonRoundtrip :: HoverContents -> Property)     prop "ResponseMessage"   (propertyJsonRoundtrip :: ResponseMessage (Maybe ()) -> Property)+    prop "WatchedFiles"      (propertyJsonRoundtrip :: DidChangeWatchedFilesRegistrationOptions -> Property)   -- ---------------------------------------------------------------------@@ -106,4 +107,15 @@ instance (Arbitrary a) => Arbitrary (List a) where   arbitrary = List <$> arbitrary +-- ---------------------------------------------------------------------++instance Arbitrary DidChangeWatchedFilesRegistrationOptions where+  arbitrary = DidChangeWatchedFilesRegistrationOptions <$> arbitrary++instance Arbitrary FileSystemWatcher where+  arbitrary = FileSystemWatcher <$> arbitrary <*> arbitrary++instance Arbitrary WatchKind where+  arbitrary = WatchKind <$> arbitrary <*> arbitrary <*> arbitrary+   -- ---------------------------------------------------------------------
test/URIFilePathSpec.hs view
@@ -2,32 +2,41 @@ {-# LANGUAGE OverloadedStrings #-} module URIFilePathSpec where +import Control.Monad                          (when) import Data.List #if __GLASGOW_HASKELL__ < 808-import Data.Monoid ((<>))+import Data.Monoid                            ((<>)) #endif-import Data.Text                              (pack)+import Data.Text                              (Text, pack) import Language.Haskell.LSP.Types -import           Network.URI-import qualified System.FilePath.Windows as FPW+import Network.URI import Test.Hspec import Test.QuickCheck #if !MIN_VERSION_QuickCheck(2,10,0) import Data.Char                              (GeneralCategory(..), generalCategory) #endif-+import qualified System.FilePath.Windows as FPW+import System.FilePath                        (normalise)+import qualified System.Info -- --------------------------------------------------------------------- +isWindows :: Bool+isWindows = System.Info.os == "mingw32"+ main :: IO () main = hspec spec  spec :: Spec spec = do+  describe "Platform aware URI file path functions" platformAwareUriFilePathSpec   describe "URI file path functions" uriFilePathSpec-  describe "file path URI functions" filePathUriSpec   describe "URI normalization functions" uriNormalizeSpec+  describe "Normalized file path functions" normalizedFilePathSpec +windowsOS :: String+windowsOS = "mingw32"+ testPosixUri :: Uri testPosixUri = Uri $ pack "file:///home/myself/example.hs" @@ -37,23 +46,14 @@ relativePosixFilePath :: FilePath relativePosixFilePath = "myself/example.hs" -withCurrentDirPosixFilePath :: FilePath-withCurrentDirPosixFilePath = "/home/./myself/././example.hs"- testWindowsUri :: Uri-testWindowsUri = Uri $ pack "file:///C:/Users/myself/example.hs"+testWindowsUri = Uri $ pack "file:///c:/Users/myself/example.hs"  testWindowsFilePath :: FilePath-testWindowsFilePath = "C:\\Users\\myself\\example.hs"--testWindowsFilePathDriveLowerCase :: FilePath-testWindowsFilePathDriveLowerCase = "c:\\Users\\myself\\example.hs"--withCurrentDirWindowsFilePath :: FilePath-withCurrentDirWindowsFilePath = "C:\\Users\\.\\myself\\.\\.\\example.hs"+testWindowsFilePath = "c:\\Users\\myself\\example.hs" -uriFilePathSpec :: Spec-uriFilePathSpec = do+platformAwareUriFilePathSpec :: Spec+platformAwareUriFilePathSpec = do   it "converts a URI to a POSIX file path" $ do     let theFilePath = platformAwareUriToFilePath "posix" testPosixUri     theFilePath `shouldBe` Just testPosixFilePath@@ -70,43 +70,29 @@     let theUri = platformAwareFilePathToUri windowsOS testWindowsFilePath     theUri `shouldBe` testWindowsUri -  it "removes unnecesary current directory paths" $ do-    let theUri = platformAwareFilePathToUri "posix" withCurrentDirPosixFilePath-    theUri `shouldBe` testPosixUri--  it "removes unnecesary current directory paths in windows" $ do-    let theUri = platformAwareFilePathToUri windowsOS withCurrentDirWindowsFilePath-    theUri `shouldBe` testWindowsUri--  it "make the drive letter upper case when converting a Windows file path to a URI" $ do-    let theUri = platformAwareFilePathToUri windowsOS testWindowsFilePathDriveLowerCase-    theUri `shouldBe` testWindowsUri--filePathUriSpec :: Spec-filePathUriSpec = do   it "converts a POSIX file path to a URI" $ do     let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"-    theFilePath `shouldBe` (Uri "file://Functional.hs")+    theFilePath `shouldBe` (Uri "file://./Functional.hs")    it "converts a Windows file path to a URI" $ do     let theFilePath = platformAwareFilePathToUri windowsOS "./Functional.hs"-    theFilePath `shouldBe` (Uri "file:///Functional.hs")+    theFilePath `shouldBe` (Uri "file:///./Functional.hs")    it "converts a Windows file path to a URI" $ do     let theFilePath = platformAwareFilePathToUri windowsOS "c:/Functional.hs"-    theFilePath `shouldBe` (Uri "file:///C:/Functional.hs")+    theFilePath `shouldBe` (Uri "file:///c:/Functional.hs")    it "converts a POSIX file path to a URI and back" $ do     let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"-    theFilePath `shouldBe` (Uri "file://Functional.hs")-    let Just (URI scheme' auth' path' query' frag') =  parseURI "file://Functional.hs"+    theFilePath `shouldBe` (Uri "file://./Functional.hs")+    let Just (URI scheme' auth' path' query' frag') =  parseURI "file://./Functional.hs"     (scheme',auth',path',query',frag') `shouldBe`       ("file:"-      ,Just (URIAuth {uriUserInfo = "", uriRegName = "Functional.hs", uriPort = ""}) -- AZ: Seems odd-      ,""+      ,Just (URIAuth {uriUserInfo = "", uriRegName = ".", uriPort = ""}) -- AZ: Seems odd+      ,"/Functional.hs"       ,""       ,"")-    Just "Functional.hs" `shouldBe` platformAwareUriToFilePath "posix" theFilePath+    Just "./Functional.hs" `shouldBe` platformAwareUriToFilePath "posix" theFilePath    it "converts a Posix file path to a URI and back" $ property $ forAll genPosixFilePath $ \fp -> do       let uri = platformAwareFilePathToUri "posix" fp@@ -115,7 +101,9 @@   it "converts a Windows file path to a URI and back" $ property $ forAll genWindowsFilePath $ \fp -> do       let uri = platformAwareFilePathToUri windowsOS fp       -- We normalise to account for changes in the path separator.-      platformAwareUriToFilePath windowsOS uri `shouldBe` Just (FPW.normalise fp)+      -- But driver letters are *not* normalized so we skip them+      when (not $ "c:" `isPrefixOf` fp) $+        platformAwareUriToFilePath windowsOS uri `shouldBe` Just (FPW.normalise fp)    it "converts a relative POSIX file path to a URI and back" $ do     let uri = platformAwareFilePathToUri "posix" relativePosixFilePath@@ -123,15 +111,135 @@     let back = platformAwareUriToFilePath "posix" uri     back `shouldBe` Just relativePosixFilePath ++testUri :: Uri+testUri | isWindows = Uri "file:///C:/Users/myself/example.hs"+        | otherwise = Uri "file:///home/myself/example.hs"++testFilePath :: FilePath+testFilePath | isWindows = "C:\\Users\\myself\\example.hs"+             | otherwise = "/home/myself/example.hs"++withCurrentDirFilePath :: FilePath+withCurrentDirFilePath | isWindows = "C:\\Users\\.\\myself\\.\\.\\example.hs"+                       | otherwise = "/home/./myself/././example.hs"++fromRelativefilePathUri :: Uri+fromRelativefilePathUri | isWindows = Uri  "file:///myself/example.hs"+                        | otherwise = Uri "file://myself/example.hs"++relativeFilePath :: FilePath+relativeFilePath | isWindows = "myself\\example.hs"+                 | otherwise = "myself/example.hs"++withLowerCaseDriveLetterFilePath :: FilePath+withLowerCaseDriveLetterFilePath = "c:\\Users\\.\\myself\\.\\.\\example.hs"++withInitialCurrentDirUriStr :: String+withInitialCurrentDirUriStr | isWindows = "file:///Functional.hs"+                            | otherwise = "file://Functional.hs"++withInitialCurrentDirUriParts :: (String, Maybe URIAuth,  String, String, String)+withInitialCurrentDirUriParts+  | isWindows =+    ("file:"+    ,Just (URIAuth {uriUserInfo = "", uriRegName = "", uriPort = ""}) -- JNS: And asymmetrical+    ,"/Functional.hs","","")+  | otherwise =+     ("file:"+    ,Just (URIAuth {uriUserInfo = "", uriRegName = "Functional.hs", uriPort = ""}) -- AZ: Seems odd+    ,"","","")++withInitialCurrentDirFilePath :: FilePath+withInitialCurrentDirFilePath | isWindows = ".\\Functional.hs"+                              | otherwise = "./Functional.hs"++noNormalizedUriTxt :: Text+noNormalizedUriTxt | isWindows = "file:///c:/Users/./myself/././example.hs"+                   | otherwise = "file:///home/./myself/././example.hs"++noNormalizedUri :: Uri+noNormalizedUri = Uri noNormalizedUriTxt++uriFilePathSpec :: Spec+uriFilePathSpec = do+  it "converts a URI to a file path" $ do+    let theFilePath = uriToFilePath testUri+    theFilePath `shouldBe` Just testFilePath++  it "converts a file path to a URI" $ do+    let theUri = filePathToUri testFilePath+    theUri `shouldBe` testUri++  it "removes unnecesary current directory paths" $ do+    let theUri = filePathToUri withCurrentDirFilePath+    theUri `shouldBe` testUri++  when isWindows $+    it "make the drive letter upper case when converting a Windows file path to a URI" $ do+      let theUri = filePathToUri withLowerCaseDriveLetterFilePath+      theUri `shouldBe` testUri++  it "converts a file path to a URI and back" $ property $ forAll genFilePath $ \fp -> do+      let uri = filePathToUri fp+      uriToFilePath uri `shouldBe` Just (normalise fp)++  it "converts a relative file path to a URI and back" $ do+    let uri = filePathToUri relativeFilePath+    uri `shouldBe` fromRelativefilePathUri+    let back = uriToFilePath uri+    back `shouldBe` Just relativeFilePath++  it "converts a file path with initial current dir to a URI and back" $ do+    let uri = filePathToUri withInitialCurrentDirFilePath+    uri `shouldBe` (Uri (pack withInitialCurrentDirUriStr))+    let Just (URI scheme' auth' path' query' frag') =  parseURI withInitialCurrentDirUriStr+    (scheme',auth',path',query',frag') `shouldBe` withInitialCurrentDirUriParts+    Just "Functional.hs" `shouldBe` uriToFilePath uri+ uriNormalizeSpec :: Spec uriNormalizeSpec = do-  it "ignores differences in percent-encoding" $ property $ \uri -> do++  it "ignores differences in percent-encoding" $ property $ \uri ->     toNormalizedUri (Uri $ pack $ escapeURIString isUnescapedInURI uri) `shouldBe`         toNormalizedUri (Uri $ pack $ escapeURIString (const False) uri) +  it "ignores differences in percent-encoding (examples)" $ do+    toNormalizedUri (Uri $ pack "http://server/path%C3%B1?param=%C3%B1") `shouldBe`+        toNormalizedUri (Uri $ pack "http://server/path%c3%b1?param=%c3%b1")+    toNormalizedUri (Uri $ pack "file:///path%2A") `shouldBe`+        toNormalizedUri (Uri $ pack "file:///path%2a")++  it "normalizes uri file path when converting from uri to normalized uri" $ do+    let (NormalizedUri _ uri) = toNormalizedUri noNormalizedUri+    let (Uri nuri) = testUri+    uri `shouldBe` nuri++  it "converts a file path with reserved uri chars to a normalized URI and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "path;part#fragmen?param=val"+    let nuri = toNormalizedUri (filePathToUri fp)+    uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp++  it "converts a file path with substrings that looks like uri escaped chars and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "ca%C3%B1a"+    let nuri = toNormalizedUri (filePathToUri fp)+    uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp++  it "converts a file path to a normalized URI and back" $ property $ forAll genFilePath $ \fp -> do+    let nuri = toNormalizedUri (filePathToUri fp)+    case uriToFilePath (fromNormalizedUri nuri) of+      Just nfp -> nfp `shouldBe` (normalise fp)+      Nothing -> return () -- Some unicode paths creates invalid uris, ignoring for now++genFilePath :: Gen FilePath+genFilePath | isWindows = genWindowsFilePath+            | otherwise = genPosixFilePath+ genWindowsFilePath :: Gen FilePath genWindowsFilePath = do-    segments <- listOf pathSegment+    segments <- listOf1 pathSegment     pathSep <- elements ['/', '\\']     driveLetter <- elements ["C:", "c:"]     pure (driveLetter <> [pathSep] <> intercalate [pathSep] segments)@@ -139,7 +247,7 @@  genPosixFilePath :: Gen FilePath genPosixFilePath = do-    segments <- listOf pathSegment+    segments <- listOf1 pathSegment     pure ("/" <> intercalate "/" segments)   where pathSegment = listOf1 (genValidUnicodeChar `suchThat` (`notElem` ['/'])) @@ -154,3 +262,32 @@   where     isSurrogate c = generalCategory c == Surrogate #endif++normalizedFilePathSpec :: Spec+normalizedFilePathSpec = do+  it "makes file path normalized" $ property $ forAll genFilePath $ \fp -> do+    let nfp = toNormalizedFilePath fp+    fromNormalizedFilePath nfp `shouldBe` (normalise fp)++  it "converts to a normalized uri and back" $ property $ forAll genFilePath $ \fp -> do+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    case uriToNormalizedFilePath nuri of+      Just nfp -> fromNormalizedFilePath nfp `shouldBe` (normalise fp)+      Nothing -> return () -- Some unicode paths creates invalid uris, ignoring for now++  it "converts a file path with reserved uri chars to a normalized URI and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "path;part#fragmen?param=val"+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp++  it "converts a file path with substrings that looks like uri escaped chars and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "ca%C3%B1a"+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp++  it "creates the same NormalizedUri than the older implementation" $ property $ forAll genFilePath $ \fp -> do+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    let oldNuri = toNormalizedUri (filePathToUri fp)+    nuri `shouldBe` oldNuri
test/VspSpec.hs view
@@ -322,3 +322,6 @@        pp14 <- getCompletionPrefix (J.Position 1 14) (vfsFromText orig)       pp14 `shouldBe` Just (PosPrefixInfo "import Data.List" "Data" "Li" (J.Position 1 14))++      pp00 <- getCompletionPrefix (J.Position 0 0) (vfsFromText orig)+      pp00 `shouldBe` Just (PosPrefixInfo "{-# ings #-}" "" "" (J.Position 0 0))