Add File System Path to the Content Roots (#1827)

This commit is contained in:
Radosław Waśko 2021-07-05 11:23:29 +02:00 committed by GitHub
parent 0b347d7443
commit 8d71145d57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 177 deletions

View File

@ -57,6 +57,7 @@ transport formats, please look [here](./protocol-architecture).
- [`WorkspaceEdit`](#workspaceedit)
- [`EnsoDigest`](#ensodigest)
- [`FileSegment`](#filesegment)
- [`ContentRoot`](#contentroot)
- [Connection Management](#connection-management)
- [`session/initProtocolConnection`](#sessioninitprotocolconnection)
- [`session/initBinaryConnection`](#sessioninitbinaryconnection)
@ -1156,35 +1157,61 @@ a location on a real file-system that has been virtualised for use in the Enso
VFS.
```typescript
interface ContentRoot {
// A unique identifier for the content root.
id: UUID;
// The type of content root.
type: ContentRootType;
// The name of the content root.
name: String;
}
type ContentRoot = Project | FileSystemRoot | Home | Library | Custom;
```
### `ContentRootType`
The type of the annotated content root.
```typescript
type ContentRootType = Project | Root | Home | Library | Custom;
/** This content root points to the project home. */
interface Project {
// A unique identifier for the content root.
id: UUID;
}
/**
* This content root points to the system root (`/`) on unix systems, or to a
* drive root on Windows. In Windows' case, there may be multiple `Root` entries
* corresponding to the various drives.
*/
interface FileSystemRoot {
// A unique identifier for the content root.
id: UUID;
// The absolute filesystem path of the content root.
path: String;
}
/** The user's home directory. */
interface Home {
// A unique identifier for the content root.
id: UUID;
}
/** An Enso library location. */
interface Library {
// A unique identifier for the content root.
id: UUID;
// The namespace of the library.
namespace: String;
// The name of the library.
name: String;
/**
* The version of the library.
*
* It is either a semver version of the library or the string "local".
*/
version: String;
}
/** A content root that has been added by the IDE (unused for now). */
interface Custom {
// A unique identifier for the content root.
id: UUID;
}
```
These represent:
- `Project`: This content root points to the project home.
- `Root`: This content root points to the system root (`/`) on unix systems, or
to a drive root on Windows. In Windows' case, there may be multiple `Root`
entries corresponding to the various drives.
- `Home`: The user's home directory.
- `Library`: An Enso library location.
- `Custom`: A content root that has been added by the IDE (unused for now).
## Connection Management
In order to properly set-up and tear-down the language server connection, we

View File

@ -9,10 +9,10 @@ import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data._
import org.enso.languageserver.effect.ZioExec
import org.enso.languageserver.filemanager.{
ContentRoot,
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile,
FileManager,
FileSystem,
@ -62,9 +62,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
val directoriesConfig = ProjectDirectoriesConfig(serverConfig.contentRootPath)
private val contentRoot = ContentRootWithFile(
serverConfig.contentRootUuid,
ContentRootType.Project,
"Project",
ContentRoot.Project(serverConfig.contentRootUuid),
new File(serverConfig.contentRootPath)
)
val languageServerConfig = Config(

View File

@ -1,69 +1,128 @@
package org.enso.languageserver.filemanager
import enumeratum._
import io.circe.syntax.EncoderOps
import io.circe.{Encoder, Json}
import java.io.File
import java.util.UUID
/** A representation of a content root.
*
* @param id the unique identifier of the content root
* @param type the type of the content root
* @param name The name of the content root
*/
case class ContentRoot(id: UUID, `type`: ContentRootType, name: String)
/** A representation of a content root. */
sealed trait ContentRoot {
/** The type of entity that the content root represents.
*/
sealed trait ContentRootType extends EnumEntry
object ContentRootType extends Enum[ContentRootType] with CirceEnum[ContentRootType] {
/** The content root represents the root of the current Enso project.
*/
case object Project extends ContentRootType
/** The content root represents a system root (`/` on unix, drives on
* windows).
*
* There may be multiple of this type of root sent by default.
*/
case object Root extends ContentRootType
/** The content root represents the user's home directory.
*/
case object Home extends ContentRootType
/** The content root represents an Enso library.
*/
case object Library extends ContentRootType
/** The content root was a custom location added by the IDE.
*/
case object Custom extends ContentRootType
/** Necessary for Enumeratum and Circe. */
override val values = findValues
/** The unique identifier of the content root. */
def id: UUID
}
/** A representation of a content root.
object ContentRoot {
/** A filesystem root.
*
* @param id the unique identifier of the content root
* @param path absolute path of the content root
*/
case class FileSystemRoot(override val id: UUID, path: String)
extends ContentRoot
/** A root representing user's home on the filesystem.
*
* @param id the unique identifier of the content root
*/
case class Home(override val id: UUID) extends ContentRoot
/** Main project root.
*
* @param id the unique identifier of the content root
*/
case class Project(override val id: UUID) extends ContentRoot
/** A root of an imported library.
*
* @param id the unique identifier of the content root
* @param namespace namespace of the library
* @param name name of the library
* @param version version of the library
*/
case class Library(
override val id: UUID,
namespace: String,
name: String,
version: String
) extends ContentRoot
/** A custom root, currently not used.
*
* @param id the unique identifier of the content root
*/
case class Custom(override val id: UUID) extends ContentRoot
private object CodecField {
val Id = "id"
val Type = "type"
val Namespace = "namespace"
val Name = "name"
val Version = "version"
val Path = "path"
}
private object CodecType {
val FileSystemRoot = "FileSystemRoot"
val Home = "Home"
val Project = "Project"
val Library = "Library"
val Custom = "Custom"
}
/** An [[Encoder]] instance for [[ContentRoot]]. */
implicit val encoder: Encoder[ContentRoot] = {
case FileSystemRoot(id, path) =>
Json.obj(
CodecField.Type -> CodecType.FileSystemRoot.asJson,
CodecField.Id -> id.asJson,
CodecField.Path -> path.asJson
)
case Home(id) =>
Json.obj(
CodecField.Type -> CodecType.Home.asJson,
CodecField.Id -> id.asJson
)
case Project(id) =>
Json.obj(
CodecField.Type -> CodecType.Project.asJson,
CodecField.Id -> id.asJson
)
case Library(id, namespace, name, version) =>
Json.obj(
CodecField.Type -> CodecType.Library.asJson,
CodecField.Id -> id.asJson,
CodecField.Namespace -> namespace.asJson,
CodecField.Name -> name.asJson,
CodecField.Version -> version.asJson
)
case Custom(id) =>
Json.obj(
CodecField.Type -> CodecType.Custom.asJson,
CodecField.Id -> id.asJson
)
}
}
/** A representation of a content root with a file that represents its
* filesystem location
*
* @param id the unique identifier of the content root
* @param `type` the type of the content root
* @param name The name of the content root
* @param contentRoot the raw content root
* @param file the file on the filesystem that is the content root
*/
case class ContentRootWithFile(
id: UUID,
`type`: ContentRootType,
name: String,
contentRoot: ContentRoot,
file: File
) {
/** The unique identifier of the content root. */
def id: UUID = contentRoot.id
/** Convert this to a content root for use in the protocol.
*
* @return a protocol content root
*/
def toContentRoot: ContentRoot = {
ContentRoot(id, `type`, name)
}
def toContentRoot: ContentRoot = contentRoot
}

View File

@ -58,13 +58,14 @@ class ContentRootManagerActor(config: Config)
context.become(mainStage(contentRoots, subscribers + sender()))
case Api.LibraryLoaded(libraryName, libraryVersion, rootPath) =>
val rootName = s"$libraryName:$libraryVersion"
val libraryRoot = ContentRootWithFile(
id = UUID.randomUUID(),
`type` = ContentRootType.Library,
name = rootName,
file = rootPath.getCanonicalFile
ContentRoot.Library(
id = UUID.randomUUID(),
namespace = libraryName.namespace,
name = libraryName.name,
version = libraryVersion.toString
),
file = rootPath.getCanonicalFile
)
subscribers.foreach { subscriber =>
@ -109,7 +110,7 @@ object ContentRootManagerActor {
private case class ContentRoots(
projectRoot: ContentRootWithFile,
librariesRoots: List[ContentRootWithFile],
homeRoots: List[ContentRootWithFile],
homeRoot: Option[ContentRootWithFile],
filesystemRoots: List[ContentRootWithFile]
) {
def addLibraryRoot(contentRoot: ContentRootWithFile): ContentRoots =
@ -122,7 +123,7 @@ object ContentRootManagerActor {
* roots will take precedence.
*/
lazy val toList: List[ContentRootWithFile] =
List(projectRoot) ++ librariesRoots ++ homeRoots ++ filesystemRoots
List(projectRoot) ++ librariesRoots ++ homeRoot.toList ++ filesystemRoots
/** Resolves the path as relative to one of the registered content roots.
*
@ -146,13 +147,12 @@ object ContentRootManagerActor {
val fsRoots = FileSystems.getDefault.getRootDirectories.asScala.map {
path =>
val absolutePath = path.toAbsolutePath.normalize
val name =
Option(absolutePath.getRoot).map(_.toString).getOrElse("<root>")
ContentRootWithFile(
id = UUID.randomUUID(),
`type` = ContentRootType.Root,
name = name,
file = absolutePath.toFile
ContentRoot.FileSystemRoot(
id = UUID.randomUUID(),
path = absolutePath.toString
),
file = absolutePath.toFile
)
}
@ -160,16 +160,14 @@ object ContentRootManagerActor {
homeProp <- sys.props.get("user.home")
homePath <- Try(JPath.of(homeProp)).toOption
} yield ContentRootWithFile(
id = UUID.randomUUID(),
`type` = ContentRootType.Home,
name = "Home",
file = homePath.toAbsolutePath.normalize.toFile
ContentRoot.Home(UUID.randomUUID()),
file = homePath.toAbsolutePath.normalize.toFile
)
ContentRoots(
projectRoot = config.projectContentRoot,
librariesRoots = Nil,
homeRoots = homeRoot.toList,
homeRoot = homeRoot,
filesystemRoots = fsRoots.toList
)
}

View File

@ -207,7 +207,11 @@ class JsonConnectionController(
): Receive = {
case ContentRootManagerProtocol.ContentRootsAddedNotification(roots) =>
val allRoots = roots ++ rootsSoFar
if (roots.exists(_.`type` == ContentRootType.Project)) {
val hasProject = roots.exists {
case ContentRootWithFile(ContentRoot.Project(_), _) => true
case _ => false
}
if (hasProject) {
cancellable.cancel()
unstashAll()

View File

@ -5,10 +5,7 @@ import akka.testkit._
import org.apache.commons.io.FileUtils
import org.enso.languageserver.data._
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{
ContentRootType,
ContentRootWithFile
}
import org.enso.languageserver.filemanager.{ContentRoot, ContentRootWithFile}
import org.enso.searcher.sql.{
SchemaVersion,
SqlDatabase,
@ -222,9 +219,7 @@ class RepoInitializationSpec
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
val config = newConfig(
ContentRootWithFile(
UUID.randomUUID(),
ContentRootType.Project,
"Project",
ContentRoot.Project(UUID.randomUUID()),
testContentRoot.toFile
)
)

View File

@ -33,9 +33,7 @@ class ContentRootManagerSpec
def makeContentRootManager(): (ContentRootManagerWrapper, ActorRef) = {
val root = ContentRootWithFile(
UUID.randomUUID(),
ContentRootType.Project,
"Project",
ContentRoot.Project(UUID.randomUUID()),
new File("foobar").getCanonicalFile
)
val config = Config(
@ -57,17 +55,17 @@ class ContentRootManagerSpec
val (contentRootManager, _) = makeContentRootManager()
val roots =
contentRootManager.getContentRoots(system.dispatcher).futureValue
val simplifiedRoots =
roots.map(root => (root.name, root.`type`, root.file))
val fsRoots =
roots.collect {
case ContentRootWithFile(ContentRoot.FileSystemRoot(_, path), _) =>
path
}
if (SystemUtils.IS_OS_WINDOWS) {
simplifiedRoots should contain(
("C:\\", ContentRootType.Root, new File("C:\\"))
)
fsRoots should contain("C:\\")
} else {
simplifiedRoots should contain(
("/", ContentRootType.Root, new File("/"))
)
fsRoots should contain("/")
fsRoots should have size 1
}
}
@ -79,15 +77,19 @@ class ContentRootManagerSpec
inside(subscriberProbe.receiveOne(2.seconds.dilated)) {
case ContentRootsAddedNotification(roots) =>
val projectRoot = roots.filter(_.`type` == ContentRootType.Project)
projectRoot should have size 1
roots.filter(_.`type` == ContentRootType.Root) should not be empty
val projectRoots = roots.collect {
case ContentRootWithFile(ContentRoot.Project(_), _) =>
}
val fsRoots = roots.collect {
case ContentRootWithFile(ContentRoot.Project(_), _) =>
}
projectRoots should have size 1
fsRoots should not be empty
}
val libraryName = LibraryName("Foo", "Bar")
val libraryVersion = LibraryVersion.Local
val rootPath = new File("foobar")
val rootName = "Foo.Bar:local"
system.eventStream.publish(
Api.LibraryLoaded(libraryName, libraryVersion, rootPath)
@ -97,13 +99,27 @@ class ContentRootManagerSpec
case ContentRootsAddedNotification(roots) =>
roots should have length 1
val root = roots.head
root.name shouldEqual rootName
root.file.getCanonicalFile shouldEqual rootPath.getCanonicalFile
root.`type` shouldEqual ContentRootType.Library
inside(root) {
case ContentRootWithFile(
ContentRoot.Library(_, namespace, name, version),
file
) =>
file.getCanonicalFile shouldEqual rootPath.getCanonicalFile
namespace shouldEqual "Foo"
name shouldEqual "Bar"
version shouldEqual "local"
}
}
val roots = wrapper.getContentRoots(system.dispatcher).futureValue
roots.map(r => r.name) should contain(rootName)
roots.exists {
case ContentRootWithFile(
ContentRoot.Library(_, "Foo", "Bar", "local"),
_
) =>
true
case _ => false
}
}
"return the root based on the id" in {
@ -124,8 +140,15 @@ class ContentRootManagerSpec
import system.dispatcher
val roots = rootManager.getContentRoots.futureValue
val projectRoot = roots.filter(_.`type` == ContentRootType.Project).head
val fsRoot = roots.filter(_.`type` == ContentRootType.Root).head
val projectRoots = roots.collect {
case root @ ContentRootWithFile(ContentRoot.Project(_), _) => root
}
val fsRoots = roots.collect {
case root @ ContentRootWithFile(ContentRoot.Project(_), _) => root
}
val projectRoot = projectRoots.head
val someFsRoot = fsRoots.head
val projectPathRel = JPath.of("p1/foo")
val projectPathAbsolute =
@ -137,10 +160,10 @@ class ContentRootManagerSpec
val fsPathRel = JPath.of("fs/bar")
val fsPathAbsolute =
fsRoot.file.toPath.resolve(fsPathRel).toFile
someFsRoot.file.toPath.resolve(fsPathRel).toFile
val fsPathResolved =
rootManager.findRelativePath(fsPathAbsolute).futureValue.value
fsPathResolved.rootId shouldEqual fsRoot.id
fsPathResolved.rootId shouldEqual someFsRoot.id
fsPathResolved.segments shouldEqual Vector("fs", "bar")
}
}

View File

@ -0,0 +1,53 @@
package org.enso.languageserver.filemanager
import io.circe.Json
import io.circe.syntax._
import io.circe.literal._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import java.util.UUID
class ContentRootSerializationSpec extends AnyWordSpec with Matchers {
"ContentRoot" should {
"correctly serialize" in {
val id = UUID.randomUUID()
def toJson(contentRoot: ContentRoot): Json = contentRoot.asJson
toJson(ContentRoot.FileSystemRoot(id, path = "/")) shouldEqual
json"""{
"type": "FileSystemRoot",
"id": $id,
"path": "/"
}"""
toJson(ContentRoot.Project(id)) shouldEqual
json"""{
"type": "Project",
"id": $id
}"""
toJson(ContentRoot.Home(id)) shouldEqual
json"""{
"type": "Home",
"id": $id
}"""
toJson(ContentRoot.Library(id, "foo", "Bar", "baz")) shouldEqual
json"""{
"type": "Library",
"id": $id,
"namespace": "foo",
"name": "Bar",
"version": "baz"
}"""
toJson(ContentRoot.Custom(id)) shouldEqual
json"""{
"type": "Custom",
"id": $id
}"""
}
}
}

View File

@ -6,10 +6,10 @@ import org.apache.commons.io.FileUtils
import org.enso.languageserver.data._
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{
ContentRoot,
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile
}
import org.enso.languageserver.runtime.ContextRegistryProtocol._
@ -474,9 +474,7 @@ class ContextEventsListenerSpec
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
val config = newConfig(
ContentRootWithFile(
UUID.randomUUID(),
ContentRootType.Project,
"Project",
ContentRoot.Project(UUID.randomUUID()),
testContentRoot.toFile
)
)

View File

@ -1,8 +1,5 @@
package org.enso.languageserver.search
import java.io.File
import java.nio.file.Files
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import org.apache.commons.io.FileUtils
@ -12,14 +9,7 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
}
import org.enso.languageserver.data._
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile,
Path
}
import org.enso.languageserver.filemanager._
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
import org.enso.languageserver.search.SearchProtocol.SuggestionDatabaseEntry
import org.enso.languageserver.session.JsonSession
@ -36,6 +26,9 @@ import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import java.io.File
import java.nio.file.Files
import java.util.UUID
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
import scala.util.{Failure, Success}
@ -839,9 +832,7 @@ class SuggestionsHandlerSpec
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
val config = newConfig(
ContentRootWithFile(
UUID.randomUUID(),
ContentRootType.Project,
"Project",
ContentRoot.Project(UUID.randomUUID()),
testContentRoot.toFile
)
)

View File

@ -15,10 +15,10 @@ import org.enso.languageserver.data.{
}
import org.enso.languageserver.effect.ZioExec
import org.enso.languageserver.filemanager.{
ContentRoot,
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile,
FileManager,
FileSystem
@ -37,9 +37,7 @@ class BaseBinaryServerTest extends BinaryServerTestKit {
val testContentRootId = UUID.randomUUID()
val testContentRoot = ContentRootWithFile(
testContentRootId,
ContentRootType.Project,
"Project",
ContentRoot.Project(testContentRootId),
Files.createTempDirectory(null).toRealPath().toFile
)
val config = Config(

View File

@ -2,6 +2,8 @@ package org.enso.languageserver.websocket.json
import akka.testkit.TestProbe
import io.circe.literal._
import io.circe.parser.parse
import io.circe.syntax.EncoderOps
import org.apache.commons.io.FileUtils
import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
@ -14,16 +16,7 @@ import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data._
import org.enso.languageserver.effect.ZioExec
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.{
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile,
FileManager,
FileSystem,
ReceivesTreeUpdatesHandler
}
import org.enso.languageserver.filemanager._
import org.enso.languageserver.io._
import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory,
@ -37,16 +30,14 @@ import org.enso.languageserver.text.BufferRegistry
import org.enso.polyglot.data.TypeGraph
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo}
import org.enso.testkit.EitherValue
import org.enso.text.Sha3_224VersionCalculator
import org.scalatest.OptionValues
import java.nio.file.Files
import java.util.UUID
import scala.concurrent.Await
import scala.concurrent.duration._
import io.circe.parser.parse
import io.circe.syntax.EncoderOps
import org.enso.testkit.EitherValue
import org.scalatest.OptionValues
class BaseServerTest
extends JsonRpcServerTestKit
@ -59,9 +50,7 @@ class BaseServerTest
val testContentRootId = UUID.randomUUID()
val testContentRoot = ContentRootWithFile(
testContentRootId,
ContentRootType.Project,
"Project",
ContentRoot.Project(testContentRootId),
Files.createTempDirectory(null).toRealPath().toFile
)
val config = mkConfig
@ -250,8 +239,7 @@ class BaseServerTest
json"""
{
"id" : $testContentRootId,
"type" : "Project",
"name" : "Project"
"type" : "Project"
}
"""
)

View File

@ -1799,8 +1799,6 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
val libraryName = LibraryName("Foo", "Bar")
val libraryVersion = LibraryVersion.Published(SemVer(1, 2, 3), repo)
val rootPath = new File("foobar")
val rootName = "Foo.Bar:1.2.3"
system.eventStream.publish(
Api.LibraryLoaded(libraryName, libraryVersion, rootPath)
)
@ -1809,8 +1807,10 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
inside(parsed) { case Right(json) =>
val params = json.asObject.value("params").value.asObject.value
val root = params("root").value.asObject.value
root("name").value.asString.value shouldEqual rootName
root("type").value.asString.value shouldEqual "Library"
root("namespace").value.asString.value shouldEqual "Foo"
root("name").value.asString.value shouldEqual "Bar"
root("version").value.asString.value shouldEqual "1.2.3"
}
}
}

View File

@ -30,8 +30,7 @@ class SessionManagementTest extends BaseServerTest {
json"""
{
"id" : $testContentRootId,
"type" : "Project",
"name" : "Project"
"type" : "Project"
}
"""
)

View File

@ -4,14 +4,16 @@ import io.circe.{Decoder, DecodingFailure}
/** Represents a library name that should uniquely identify the library.
*
* The prefix is either a special prefix or a username.
* @param namespace library's namespace - either a special reserved prefix or
* the username of the main author
* @param name library's name
*/
case class LibraryName(prefix: String, name: String) {
case class LibraryName(namespace: String, name: String) {
/** The qualified name of the library consists of its prefix and name
* separated with a dot.
*/
def qualifiedName: String = s"$prefix.$name"
def qualifiedName: String = s"$namespace.$name"
/** @inheritdoc */
override def toString: String = qualifiedName

View File

@ -17,7 +17,7 @@ class LibraryNameSpec
val libraryName = LibraryName.fromString(str).rightValue
libraryName.qualifiedName shouldEqual str
libraryName.name shouldEqual "Bar"
libraryName.prefix shouldEqual "Foo"
libraryName.namespace shouldEqual "Foo"
val yamlParsed = YamlHelper.parseString[LibraryName](str).rightValue
yamlParsed shouldEqual libraryName

View File

@ -28,7 +28,7 @@ class DefaultLocalLibraryProvider(distributionManager: DistributionManager)
searchPaths: List[Path]
): Option[Path] = searchPaths match {
case head :: tail =>
val potentialPath = head / libraryName.prefix / libraryName.name
val potentialPath = head / libraryName.namespace / libraryName.name
if (Files.exists(potentialPath) && Files.isDirectory(potentialPath))
Some(potentialPath)
else findLibraryHelper(libraryName, tail)