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 +11/−0
- example/Main.hs +15/−9
- haskell-lsp.cabal +2/−2
- src/Language/Haskell/LSP/Core.hs +7/−5
- src/Language/Haskell/LSP/VFS.hs +5/−3
- test/DiagnosticsSpec.hs +2/−2
- test/JsonSpec.hs +12/−0
- test/URIFilePathSpec.hs +182/−45
- test/VspSpec.hs +3/−0
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))