Skip to content

Commit

Permalink
Support for Multiple Content Roots (#1821)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeusgd authored and iamrecursion committed Jul 9, 2021
1 parent 8eb05c4 commit 32b459e
Show file tree
Hide file tree
Showing 48 changed files with 1,335 additions and 577 deletions.
4 changes: 4 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
- Fixed a bug where the Project Manager would not preinstall the Graal runtime
if the engine was already installed and only its runtime was missing
([#1824](https://github.com/enso-org/enso/pull/1824)).
- Extended content root mechanism to provide the home directory and filesystem
roots on startup ([#1821](https://github.com/enso-org/enso/pull/1821)). It now
also supports dynamically adding content roots and notifies the IDE when a new
content root is added.

# Enso 0.2.12 (2021-06-24)

Expand Down
80 changes: 10 additions & 70 deletions docs/language-server/protocol-language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -2155,76 +2155,17 @@ of the (possibly multiple) content roots.

None

### `file/addRoot`

This request adds a content root to the active project.

- **Type:** Request
- **Direction:** Client -> Server
- **Connection:** Protocol
- **Visibility:** Public

When a content root is added, the language server must notify clients other than
the one that added the root by sending a `file/rootAdded`. Additionally, all
clients must be notified with a `file/event` about the addition of the new root.
The IDE is responsible for calling `file/tree` on that root to discover its
structure.

#### Parameters

```typescript
{
absolutePath: [String];
id: UUID; // The ID of the content root
}
```

#### Result

```typescript
null;
```

#### Errors

TBC

### `file/removeRoot`

This request removes a content root from the active project.

- **Type:** Request
- **Direction:** Client -> Server
- **Connection:** Protocol
- **Visibility:** Public

When a content root is removed, the language server must notify clients other
than the one that added the root by sending a `file/rootRemoved`. Additionally,
the server must send a `file/event` making the root of the new tree visible. The
IDE is responsible for any additional discovery.

#### Parameters

```typescript
{
id: UUID; // The content root ID
}
```

#### Result

```typescript
null;
```

#### Errors

TBC

### `file/rootAdded`

This is a notification sent to all clients other than the one performing the
addition of the root in order to inform them of the content root's ID.
This is a notification sent to all clients to inform them that a content root
has been added.

At the beginning, a series of notifications is sent that lists all content roots
that are present at the current moment. This message may contain the same
content roots that were already present in the `session/initProtocolConnection`.
That is done, because there is no guarantee that no root has been added between
the init message and the time when notifications start being sent, and this
ensures that no content root is missed.

- **Type:** Notification
- **Direction:** Server -> Client
Expand All @@ -2235,8 +2176,7 @@ addition of the root in order to inform them of the content root's ID.

```typescript
{
id: UUID; // The content root ID
absolutePath: [String];
root: ContentRoot;
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import org.enso.languageserver.capability.CapabilityRouter
import org.enso.languageserver.data._
import org.enso.languageserver.effect.ZioExec
import org.enso.languageserver.filemanager.{
ContentRootManager,
ContentRootManagerActor,
ContentRootManagerWrapper,
ContentRootType,
ContentRootWithFile,
FileManager,
Expand Down Expand Up @@ -65,7 +68,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
new File(serverConfig.contentRootPath)
)
val languageServerConfig = Config(
Map(serverConfig.contentRootUuid -> contentRoot),
contentRoot,
FileManagerConfig(timeout = 3.seconds),
PathWatcherConfig(),
ExecutionContextConfig(),
Expand Down Expand Up @@ -116,8 +119,22 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
lazy val runtimeConnector =
system.actorOf(RuntimeConnector.props, "runtime-connector")

lazy val contentRootManagerActor =
system.actorOf(
ContentRootManagerActor.props(languageServerConfig),
"content-root-manager"
)

lazy val contentRootManagerWrapper: ContentRootManager =
new ContentRootManagerWrapper(languageServerConfig, contentRootManagerActor)

lazy val fileManager = system.actorOf(
FileManager.pool(languageServerConfig, fileSystem, zioExec),
FileManager.pool(
languageServerConfig.fileManager,
contentRootManagerWrapper,
fileSystem,
zioExec
),
"file-manager"
)

Expand All @@ -129,8 +146,12 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {

lazy val receivesTreeUpdatesHandler =
system.actorOf(
ReceivesTreeUpdatesHandler
.props(languageServerConfig, fileSystem, zioExec),
ReceivesTreeUpdatesHandler.props(
languageServerConfig,
contentRootManagerWrapper,
fileSystem,
zioExec
),
"file-event-registry"
)

Expand All @@ -139,6 +160,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
SuggestionsHandler
.props(
languageServerConfig,
contentRootManagerWrapper,
suggestionsRepo,
versionsRepo,
sessionRouter,
Expand All @@ -163,6 +185,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
.props(
suggestionsRepo,
languageServerConfig,
RuntimeFailureMapper(contentRootManagerWrapper),
runtimeConnector,
sessionRouter
),
Expand Down Expand Up @@ -249,6 +272,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
bufferRegistry,
capabilityRouter,
fileManager,
contentRootManagerActor,
contextRegistry,
suggestionsHandler,
stdOutController,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package org.enso.languageserver.data

import java.io.File
import java.nio.file.Files
import java.util.UUID
import org.enso.languageserver.filemanager.{
ContentRootNotFound,
ContentRootWithFile,
FileSystemFailure,
Path
}
import org.enso.languageserver.filemanager.ContentRootWithFile
import org.enso.logger.masking.{MaskingUtils, ToLogString}

import java.io.File
import java.nio.file.Files
import scala.concurrent.duration._

/** Configuration of the path watcher.
Expand Down Expand Up @@ -123,15 +117,14 @@ object ProjectDirectoriesConfig {

/** The config of the running Language Server instance.
*
* @param contentRoots a mapping between content root id and absolute path to
* the content root
* @param projectContentRoot project's main content root
* @param fileManager the file manager config
* @param pathWatcher the path watcher config
* @param executionContext the executionContext config
* @param directories the configuration of internal directories
*/
case class Config(
contentRoots: Map[UUID, ContentRootWithFile],
projectContentRoot: ContentRootWithFile,
fileManager: FileManagerConfig,
pathWatcher: PathWatcherConfig,
executionContext: ExecutionContextConfig,
Expand All @@ -140,40 +133,20 @@ case class Config(

/** @inheritdoc */
override def toLogString(shouldMask: Boolean): String = {
val maskedRoots =
val maskedRoot =
if (shouldMask) {
contentRoots
.map { case (k, v) =>
k -> MaskingUtils.toMaskedPath(v.file.toPath)
}
MaskingUtils.toMaskedPath(projectContentRoot.file.toPath)
} else {
contentRoots
projectContentRoot
}
s"Config(" +
s"contentRoots=$maskedRoots, " +
s"projectContentRoot=$maskedRoot, " +
s"fileManager=$fileManager, " +
s"pathWatcher=$pathWatcher, " +
s"executionContext=$executionContext, " +
s"directories=${directories.toLogString(shouldMask)}" +
s")"
}

def findContentRoot(
rootId: UUID
): Either[FileSystemFailure, ContentRootWithFile] =
contentRoots
.get(rootId)
.toRight(ContentRootNotFound)

def findRelativePath(path: File): Option[Path] =
contentRoots.view.flatMap { case (id, root) =>
if (path.toPath.startsWith(root.file.toPath)) {
Some(Path(id, root.file.toPath.relativize(path.toPath)))
} else {
None
}
}.headOption

}
object Config {
def ensoPackageConfigName: String = "package.yaml"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.enso.languageserver.filemanager

import java.io.File
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}

/** The external interface of the Content Root Manager that can be used by other
* components.
*
* This interface encapsulates the underlying Actor-based implementation and
* only exposes the operations that are simple requests. To subscribe for
* content root updates, the Actor interface should be used directly.
*/
trait ContentRootManager {

/** Lists the content roots currently available.
*
* @param ec the execution context on which to wait for the reply from the
* content root manager
* @return a future that will complete with a list of currently available
* content roots
*/
def getContentRoots(implicit
ec: ExecutionContext
): Future[List[ContentRootWithFile]]

/** Finds the content root with the given id.
*
* @param id the id of the content root to get
* @param ec the execution context on which to wait for the reply from the
* content root manager
* @return a future that will complete either with the found content root or
* with an error if a content root with the requested id was not
* found
*/
def findContentRoot(id: UUID)(implicit
ec: ExecutionContext
): Future[Either[ContentRootNotFound.type, ContentRootWithFile]]

/** Converts a filesystem path to a path relative to one of the available
* content roots.
*
* The implementation should pick the most specific content root for the
* path, i.e. the one whose root location shares the longest common prefix
* with the path that is being resolved.
*
* @param path the path to convert
* @param ec the execution context on which to wait for the reply from the
* content root manager
* @return a future that will complete with the relativized path or None if
* the path was not relative to any of the available roots
*/
def findRelativePath(path: File)(implicit
ec: ExecutionContext
): Future[Option[Path]]
}
Loading

0 comments on commit 32b459e

Please sign in to comment.