@@ -47,7 +47,7 @@ import qualified Language.LSP.Protocol.Types as LSP
4747import qualified Data.List as List
4848import System.Directory
4949import System.Environment (getExecutablePath , lookupEnv )
50- import qualified Paths_jl4_core
50+ import qualified L4.API.EmbeddedLibraries as EmbeddedLibraries
5151import qualified L4.Utils.IntervalMap as IV
5252import UnliftIO
5353
@@ -232,6 +232,62 @@ instance Pretty Log where
232232 <+> pretty s. category
233233 LogImportResolution msg -> " [Import Resolution]" <+> pretty msg
234234
235+ -- | Result of filesystem library resolution.
236+ data LibraryResolution = LibraryResolution
237+ { resolvedPath :: ! (Maybe FilePath ) -- ^ Path found, if any
238+ , searchedPaths :: ! [FilePath ] -- ^ All paths that were checked
239+ , hasExplicitPath :: ! Bool -- ^ True if JL4_LIBRARY_PATH is set (skip embedded libs)
240+ }
241+
242+ -- | Resolve a library module from the filesystem.
243+ -- Checks paths in order of priority:
244+ -- 1. JL4_LIBRARY_PATH environment variable (user/operator override)
245+ -- 2. Root directory (project-local)
246+ -- 3. Relative to importing file
247+ -- 4. XDG data directory (~/.local/share/jl4/libraries/)
248+ -- 5. Bundled with VSCode extension (../../libraries from executable)
249+ --
250+ -- When JL4_LIBRARY_PATH is set, callers should NOT fall back to embedded
251+ -- libraries — the operator has taken explicit control of the library store.
252+ resolveLibraryFromFilesystem :: FilePath -> Maybe NormalizedFilePath -> String -> IO LibraryResolution
253+ resolveLibraryFromFilesystem rootDirectory mImportingFile modName = do
254+ let relPath = do
255+ nfp <- mImportingFile
256+ let dir = takeDirectory (fromNormalizedFilePath nfp)
257+ pure $ dir </> modName <.> " l4"
258+ rootPath = rootDirectory </> modName <.> " l4"
259+
260+ mEnvPath <- lookupEnv " JL4_LIBRARY_PATH"
261+ let hasExplicit = Maybe. isJust mEnvPath
262+ envPaths = case mEnvPath of
263+ Just p -> [p </> modName <.> " l4" ]
264+ Nothing -> []
265+
266+ discoverPaths <- do
267+ xdgDataDir <- getXdgDirectory XdgData " jl4"
268+ let xdgPath = xdgDataDir </> " libraries" </> modName <.> " l4"
269+
270+ exePath <- getExecutablePath
271+ let exeDir = takeDirectory exePath
272+ extensionRoot = exeDir </> " .." </> " .."
273+ bundledPath = extensionRoot </> " libraries" </> modName <.> " l4"
274+
275+ pure [xdgPath, bundledPath]
276+
277+ let allPaths = envPaths <> catMaybes [Just rootPath, relPath] <> discoverPaths
278+
279+ result <- runMaybeT $ asum $
280+ flip map allPaths $ \ pth -> do
281+ exists <- liftIO (doesFileExist pth)
282+ guard exists
283+ pure pth
284+
285+ pure LibraryResolution
286+ { resolvedPath = result
287+ , searchedPaths = allPaths
288+ , hasExplicitPath = hasExplicit
289+ }
290+
235291jl4Rules :: EvaluateLazy. EvalConfig -> FilePath -> Recorder (WithPriority Log ) -> Rules ()
236292jl4Rules evalConfig rootDirectory recorder = do
237293 define shakeRecorder $ \ GetLexTokens uri -> do
@@ -302,49 +358,20 @@ jl4Rules evalConfig rootDirectory recorder = do
302358 case vfsResult of
303359 Just vfsUri -> pure $ Just vfsUri
304360 Nothing -> do
305- -- Fall back to filesystem
306- let relPath = do
307- dir <- takeDirectory . fromNormalizedFilePath <$> uriToNormalizedFilePath uri
308- pure $ dir </> modName <.> " l4"
309- rootPath = rootDirectory </> modName <.> " l4"
310-
311- -- Look for libraries in multiple locations, in order of priority:
312- -- 1. JL4_LIBRARY_PATH environment variable (user override)
313- -- 2. XDG data directory (~/.local/share/jl4/libraries/) for cabal install
314- -- 3. Bundled with VSCode extension (../../libraries from executable)
315- -- 4. Cabal's getDataDir (for cabal run during development)
316- builtinPaths <- liftIO $ do
317- -- 1. Check JL4_LIBRARY_PATH environment variable
318- mEnvPath <- lookupEnv " JL4_LIBRARY_PATH"
319- let envPaths = case mEnvPath of
320- Just p -> [p </> modName <.> " l4" ]
321- Nothing -> []
322-
323- -- 2. XDG data directory (~/.local/share/jl4/libraries/)
324- xdgDataDir <- getXdgDirectory XdgData " jl4"
325- let xdgPath = xdgDataDir </> " libraries" </> modName <.> " l4"
326-
327- -- 3. VSCode extension bundled libraries
328- exePath <- getExecutablePath
329- let exeDir = takeDirectory exePath
330- extensionRoot = exeDir </> " .." </> " .."
331- bundledPath = extensionRoot </> " libraries" </> modName <.> " l4"
332-
333- -- 4. Cabal's getDataDir (for development / cabal run)
334- dataDir <- Paths_jl4_core. getDataDir
335- let cabalPath = dataDir </> " libraries" </> modName <.> " l4"
336-
337- pure $ envPaths <> [xdgPath, bundledPath, cabalPath]
338-
339- let paths = catMaybes [Just rootPath, relPath] <> builtinPaths
340-
341- existingPath <- runMaybeT $ asum $
342- flip map paths $ \ pth -> do
343- exists <- liftIO (doesFileExist pth)
344- guard exists
345- pure pth
346-
347- pure $ fmap (toNormalizedUri . filePathToUri) existingPath
361+ let mImportingNfp = uriToNormalizedFilePath uri
362+ res <- liftIO $ resolveLibraryFromFilesystem rootDirectory mImportingNfp modName
363+ case res. resolvedPath of
364+ Just fp -> pure $ Just (toNormalizedUri $ filePathToUri fp)
365+ Nothing
366+ -- Skip embedded libs when operator has set an explicit library path
367+ | res. hasExplicitPath -> pure Nothing
368+ | otherwise ->
369+ case EmbeddedLibraries. lookupEmbeddedLibrary (Text. pack modName) of
370+ Just libContent -> do
371+ let libPath = toNormalizedFilePath (" ./" <> modName <.> " l4" )
372+ _ <- Shake. addVirtualFile libPath libContent
373+ pure $ Just (normalizedFilePathToUri libPath)
374+ Nothing -> pure Nothing
348375
349376 -- Resolve all import URIs
350377 resolvedUris <- catMaybes <$> traverse resolveImportUri importNames
@@ -422,73 +449,33 @@ jl4Rules evalConfig rootDirectory recorder = do
422449 " Found in VFS: " <> (fromNormalizedUri vfsUri). getUri
423450 pure (rangeOf a, modName, [] , vfsUris, Just (Left vfsUri))
424451 Nothing -> do
425- -- Fall back to filesystem
426- logWith recorder Debug $ LogImportResolution $
427- " Not in VFS, checking filesystem..."
428-
429- paths <- catMaybes <$> do
430- -- NOTE: if the current URI is a file uri, we first check the directory relative to the current file
431- --
432- let relPath = do
433- dir <- takeDirectory . fromNormalizedFilePath <$> uriToNormalizedFilePath uri
434- pure $ dir </> modName <.> " l4"
435-
436- let rootPath = rootDirectory </> modName <.> " l4"
437-
438- -- Look for libraries in multiple locations, in order of priority:
439- -- 1. JL4_LIBRARY_PATH environment variable (user override)
440- -- 2. XDG data directory (~/.local/share/jl4/libraries/) for cabal install
441- -- 3. Bundled with VSCode extension (../../libraries from executable)
442- -- 4. Cabal's getDataDir (for cabal run during development)
443- builtinPaths <- liftIO $ do
444- -- 1. Check JL4_LIBRARY_PATH environment variable
445- mEnvPath <- lookupEnv " JL4_LIBRARY_PATH"
446- let envPaths = case mEnvPath of
447- Just p -> [p </> modName <.> " l4" ]
448- Nothing -> []
449-
450- -- 2. Cabal's getDataDir (source tree during development, install prefix when installed)
451- dataDir <- Paths_jl4_core. getDataDir
452- let cabalPath = dataDir </> " libraries" </> modName <.> " l4"
453-
454- -- 3. XDG data directory (~/.local/share/jl4/libraries/)
455- xdgDataDir <- getXdgDirectory XdgData " jl4"
456- let xdgPath = xdgDataDir </> " libraries" </> modName <.> " l4"
457-
458- -- 4. VSCode extension bundled libraries
459- -- The VSCode extension structure is:
460- -- extension/
461- -- ├── bin/<platform>/jl4-lsp[.exe] <- executable is here
462- -- └── libraries/*.l4 <- libraries are here
463- -- So we need to go up TWO levels (../../) from the executable.
464- exePath <- getExecutablePath
465- let exeDir = takeDirectory exePath
466- let extensionRoot = exeDir </> " .." </> " .."
467- let bundledPath = extensionRoot </> " libraries" </> modName <.> " l4"
468-
469- pure $ envPaths <> [cabalPath, xdgPath, bundledPath]
470-
471- pure $ [Just rootPath, relPath] <> map Just builtinPaths
472-
473- logWith recorder Debug $ LogImportResolution $
474- " Checking filesystem paths: " <> Text. intercalate " , " (map Text. pack paths)
475-
476- existingPaths <- runMaybeT do
477-
478- let guardExists pth = do
479- exists <- liftIO (doesFileExist pth)
480- guard exists
481- pure pth
482-
483- asum $ guardExists <$> paths
484-
485- case existingPaths of
486- Just fp -> logWith recorder Info $ LogImportResolution $
487- " Found on filesystem: " <> Text. pack fp
488- Nothing -> logWith recorder Warning $ LogImportResolution $
489- " Module not found: " <> Text. pack modName
490-
491- pure (rangeOf a, modName, paths, vfsUris, fmap Right existingPaths)
452+ let mImportingNfp = uriToNormalizedFilePath uri
453+ res <- liftIO $ resolveLibraryFromFilesystem rootDirectory mImportingNfp modName
454+
455+ case res. resolvedPath of
456+ Just fp -> do
457+ logWith recorder Info $ LogImportResolution $
458+ " Found on filesystem: " <> Text. pack fp
459+ pure (rangeOf a, modName, res. searchedPaths, vfsUris, Just (Right fp))
460+ Nothing
461+ -- Skip embedded libs when operator has set an explicit library path
462+ | res. hasExplicitPath -> do
463+ logWith recorder Warning $ LogImportResolution $
464+ " Module not found (JL4_LIBRARY_PATH is set, embedded libs skipped): " <> Text. pack modName
465+ pure (rangeOf a, modName, res. searchedPaths, vfsUris, Nothing )
466+ | otherwise ->
467+ case EmbeddedLibraries. lookupEmbeddedLibrary (Text. pack modName) of
468+ Just libContent -> do
469+ logWith recorder Info $ LogImportResolution $
470+ " Found in embedded libraries: " <> Text. pack modName
471+ let libPath = toNormalizedFilePath (" ./" <> modName <.> " l4" )
472+ _ <- Shake. addVirtualFile libPath libContent
473+ let libUri = normalizedFilePathToUri libPath
474+ pure (rangeOf a, modName, res. searchedPaths, vfsUris, Just (Left libUri))
475+ Nothing -> do
476+ logWith recorder Warning $ LogImportResolution $
477+ " Module not found: " <> Text. pack modName
478+ pure (rangeOf a, modName, res. searchedPaths, vfsUris, Nothing )
492479
493480 mkImportUri (range, modName, fsPaths, vfsUris, mResult) = case mResult of
494481 Just (Left vfsUri) -> do
0 commit comments