Provide API access to the project name (#1759)

This commit is contained in:
Ara Adkins 2021-05-27 16:13:52 +01:00 committed by GitHub
parent 3890abe6fa
commit a981e72fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 350 additions and 29 deletions

View File

@ -24,6 +24,10 @@
- Added support for evaluating one-shot expressions on the result values of
arbitrary expressions ([#1749](https://github.com/enso-org/enso/pull/1749)).
This is very useful for enabling more advanced introspection in the IDE.
- Added the `workspace/projectInfo` endpoint to the language server
([#1759](https://github.com/enso-org/enso/pull/1759)). This allows the IDE to
get information about the running project in contexts where the project
manager isn't available or works differently.
## Libraries

View File

@ -915,13 +915,15 @@ lazy val `language-server` = (project in file("engine/language-server"))
new TestFramework("org.scalameter.ScalaMeterFramework")
)
)
.dependsOn(`polyglot-api`)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
.dependsOn(`text-buffer`)
.dependsOn(`searcher`)
.dependsOn(testkit % Test)
.dependsOn(`json-rpc-server`)
.dependsOn(`logging-service`)
.dependsOn(`polyglot-api`)
.dependsOn(`searcher`)
.dependsOn(`text-buffer`)
.dependsOn(`version-output`)
.dependsOn(pkg)
.dependsOn(testkit % Test)
lazy val ast = (project in file("lib/scala/ast"))
.settings(

View File

@ -98,6 +98,7 @@ transport formats, please look [here](./protocol-architecture).
- [`text/applyEdit`](#textapplyedit)
- [`text/didChange`](#textdidchange)
- [Workspace Operations](#workspace-operations)
- [`workspace/projectInfo`](#workspaceprojectinfo)
- [`workspace/undo`](#workspaceundo)
- [`workspace/redo`](#workspaceredo)
- [Monitoring](#monitoring)
@ -154,6 +155,7 @@ transport formats, please look [here](./protocol-architecture).
- [`NotFile`](#notfile)
- [`CannotOverwrite`](#cannotoverwrite)
- [`ReadOutOfBounds`](#readoutofbounds)
- [`CannotDecode`](#cannotdecode)
- [`StackItemNotFoundError`](#stackitemnotfounderror)
- [`ContextNotFoundError`](#contextnotfounderror)
- [`EmptyStackError`](#emptystackerror)
@ -2336,6 +2338,44 @@ null;
The language server also has a set of operations useful for managing the client
workspace.
### `workspace/projectInfo`
This request allows the IDE to request information about the currently open
project in situations where it does not have a project manager to connect to.
- **Type:** Request
- **Direction:** Client -> Server
- **Connection:** Protocol
- **Visibility:** Public
#### Parameters
```typescript
{
}
```
#### Result
```typescript
{
// The name of the project.
projectName: String;
// The engine version on which the project is running.
engineVersion: String;
// The version of graal on which the project is running.
graalVersion: String;
}
```
#### Errors
- [`CannotDecode`](#cannotdecode) if the project configuration cannot be
decoded.
- [`FileNotFound`](#filenotfound) if the project configuration cannot be found.
### `workspace/undo`
This request is sent from the client to the server to request that an operation
@ -3930,6 +3970,17 @@ Signals that the requested file read was out of bounds for the file's size.
}
```
### `CannotDecode`
Signals that the project configuration cannot be decoded.
```typescript
"error" : {
"code" : 1010
"message" : "Cannot decode the project configuration"
}
```
```idl
namespace org.enso.languageserver.protocol.binary;

View File

@ -35,7 +35,7 @@ transport formats, please look [here](./protocol-architecture.md).
- [`task/started`](#taskstarted)
- [`task/progress-update`](#taskprogress-update)
- [`task/finished`](#taskfinished)
- [Components Management](#components-management)
- [Runtime Version Management](#runtime-version-management)
- [`engine/list-installed`](#enginelist-installed)
- [`engine/list-available`](#enginelist-available)
- [`engine/install`](#engineinstall)
@ -47,7 +47,7 @@ transport formats, please look [here](./protocol-architecture.md).
- [Logging Service](#logging-service)
- [`logging-service/get-endpoint`](#logging-serviceget-endpoint)
- [Language Server Management](#language-server-management)
- [Errors](#errors-15)
- [Errors](#errors)
- [`MissingComponentError`](#missingcomponenterror)
- [`BrokenComponentError`](#brokencomponenterror)
- [`ProjectManagerUpgradeRequired`](#projectmanagerupgraderequired)
@ -177,7 +177,7 @@ the action.
#### Parameters
```typescript
interface ProjectOpenRequest {
{
projectId: UUID;
/**
@ -192,7 +192,7 @@ interface ProjectOpenRequest {
#### Result
```typescript
interface ProjectOpenResult {
{
/**
* The version of the started language server represented by a semver version
* string.
@ -208,6 +208,9 @@ interface ProjectOpenResult {
* The endpoint used for binary protocol.
*/
languageServerBinaryAddress: IPWithSocket;
// The name of the project as it is opened.
projectName: String;
}
```

View File

@ -247,7 +247,8 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
stdOutController,
stdErrController,
stdInController,
runtimeConnector
runtimeConnector,
languageServerConfig
)
log.trace(
"Created JSON connection controller factory [{}].",

View File

@ -173,3 +173,6 @@ case class Config(
}.headOption
}
object Config {
def ensoPackageConfigName: String = "package.yaml"
}

View File

@ -163,4 +163,7 @@ object FileManagerApi {
case object NotDirectoryError extends Error(1006, "Path is not a directory")
case object CannotDecodeError
extends Error(1010, "Cannot decode the project configuration")
}

View File

@ -1,7 +1,5 @@
package org.enso.languageserver.protocol.json
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash, Status}
import akka.pattern.pipe
import akka.util.Timeout
@ -14,6 +12,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
ReleaseCapability
}
import org.enso.languageserver.capability.CapabilityProtocol
import org.enso.languageserver.data.Config
import org.enso.languageserver.event.{
JsonSessionInitialized,
JsonSessionTerminated
@ -41,6 +40,7 @@ import org.enso.languageserver.requesthandler.visualisation.{
ExecuteExpressionHandler,
ModifyVisualisationHandler
}
import org.enso.languageserver.requesthandler.workspace.ProjectInfoHandler
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.VisualisationApi.{
@ -61,7 +61,9 @@ import org.enso.languageserver.session.SessionApi.{
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.text.TextProtocol
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo
import java.util.UUID
import scala.concurrent.duration._
/** An actor handling communications between a single client and the language
@ -88,6 +90,7 @@ class JsonConnectionController(
val stdErrController: ActorRef,
val stdInController: ActorRef,
val runtimeConnector: ActorRef,
val languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds
) extends Actor
with Stash
@ -280,7 +283,7 @@ class JsonConnectionController(
private def createRequestHandlers(
rpcSession: JsonSession
): Map[Method, Props] =
): Map[Method, Props] = {
Map(
Ping -> PingHandler.props(
List(
@ -351,8 +354,10 @@ class JsonConnectionController(
.props(stdErrController, rpcSession.clientId),
RedirectStandardError -> RedirectStdErrHandler
.props(stdErrController, rpcSession.clientId),
FeedStandardInput -> FeedStandardInputHandler.props(stdInController)
FeedStandardInput -> FeedStandardInputHandler.props(stdInController),
ProjectInfo -> ProjectInfoHandler.props(languageServerConfig)
)
}
}
@ -382,6 +387,7 @@ object JsonConnectionController {
stdErrController: ActorRef,
stdInController: ActorRef,
runtimeConnector: ActorRef,
languageServerConfig: Config,
requestTimeout: FiniteDuration = 10.seconds
): Props =
Props(
@ -397,6 +403,7 @@ object JsonConnectionController {
stdErrController,
stdInController,
runtimeConnector,
languageServerConfig,
requestTimeout
)
)

View File

@ -1,10 +1,11 @@
package org.enso.languageserver.protocol.json
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import org.enso.jsonrpc.ClientControllerFactory
import org.enso.languageserver.boot.resource.InitializationComponent
import org.enso.languageserver.data.Config
import java.util.UUID
/** Language server client controller factory.
*
@ -23,7 +24,8 @@ class JsonConnectionControllerFactory(
stdOutController: ActorRef,
stdErrController: ActorRef,
stdInController: ActorRef,
runtimeConnector: ActorRef
runtimeConnector: ActorRef,
config: Config
)(implicit system: ActorSystem)
extends ClientControllerFactory {
@ -45,7 +47,8 @@ class JsonConnectionControllerFactory(
stdOutController,
stdErrController,
stdInController,
runtimeConnector
runtimeConnector,
config
)
)
}

View File

@ -17,6 +17,7 @@ import org.enso.languageserver.search.SearchApi._
import org.enso.languageserver.runtime.VisualisationApi._
import org.enso.languageserver.session.SessionApi.InitProtocolConnection
import org.enso.languageserver.text.TextApi._
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo
object JsonRpc {
@ -62,6 +63,7 @@ object JsonRpc {
.registerRequest(Completion)
.registerRequest(Import)
.registerRequest(RenameProject)
.registerRequest(ProjectInfo)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)
.registerNotification(TextDidChange)
@ -74,5 +76,4 @@ object JsonRpc {
.registerNotification(WaitingForStandardInput)
.registerNotification(SuggestionsDatabaseUpdates)
.registerNotification(VisualisationEvaluationFailed)
}

View File

@ -0,0 +1,70 @@
package org.enso.languageserver.requesthandler.workspace
import akka.actor.{Actor, ActorLogging, Props}
import buildinfo.Info
import org.enso.jsonrpc.{Request, ResponseError, ResponseResult}
import org.enso.languageserver.data.Config
import org.enso.languageserver.filemanager.FileManagerApi
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo
import org.enso.logger.masking.MaskedPath
import org.enso.pkg.{Config => PkgConfig}
import java.io.{File, FileInputStream}
import java.nio.charset.StandardCharsets
/** A request handler for `workspace/openFile` commands.
*/
class ProjectInfoHandler(languageServerConfig: Config)
extends Actor
with ActorLogging
with UnhandledLogging {
override def receive: Receive = { case Request(ProjectInfo, id, _) =>
val projectRoot = languageServerConfig.directories.root.toPath.toFile
val configFile = new File(projectRoot, Config.ensoPackageConfigName)
if (configFile.exists()) {
val projectConfig = PkgConfig.fromYaml(
new String(
new FileInputStream(configFile).readAllBytes(),
StandardCharsets.UTF_8
)
)
if (projectConfig.isSuccess) {
val projectInfo = ProjectInfo.Result(
projectName = projectConfig.get.name,
engineVersion = Info.ensoVersion,
graalVersion = Info.graalVersion
)
sender() ! ResponseResult(
ProjectInfo,
id,
projectInfo
)
} else {
log.error(
"Could not decode the package configuration at [{}].",
MaskedPath(configFile.toPath)
)
sender() ! ResponseError(Some(id), FileManagerApi.CannotDecodeError)
}
} else {
log.error(
"Could not find the package configuration in the project at [{}].",
MaskedPath(projectRoot.toPath)
)
sender() ! ResponseError(Some(id), FileManagerApi.FileNotFoundError)
}
}
}
object ProjectInfoHandler {
/** Creates a configuration object used to create a [[ProjectInfoHandler]].
*
* @return a configuration object
*/
def props(languageServerConfig: Config): Props = Props(
new ProjectInfoHandler(languageServerConfig)
)
}

View File

@ -0,0 +1,26 @@
package org.enso.languageserver.workspace
import org.enso.jsonrpc.{HasParams, HasResult, Method}
/** The workspace management JSON RPC API provided by the language server.
*
* See [[https://github.com/enso-org/enso/blob/main/docs/language-server/README.md]]
* for message specifications.
*/
object WorkspaceApi {
case object ProjectInfo extends Method("workspace/projectInfo") {
case class Params()
case class Result(
projectName: String,
engineVersion: String,
graalVersion: String
)
implicit val hasParams = new HasParams[this.type] {
type Params = ProjectInfo.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = ProjectInfo.Result
}
}
}

View File

@ -0,0 +1,6 @@
license: APLv2
name: Standard
enso-version: default
version: "0.1.0"
author: "Enso Team <contact@enso.org>"
maintainer: "Enso Team <contact@enso.org>"

View File

@ -1,8 +1,5 @@
package org.enso.languageserver.websocket.json
import java.nio.file.Files
import java.util.UUID
import akka.testkit.TestProbe
import io.circe.literal._
import org.apache.commons.io.FileUtils
@ -37,6 +34,8 @@ import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo}
import org.enso.text.Sha3_224VersionCalculator
import java.nio.file.Files
import java.util.UUID
import scala.concurrent.Await
import scala.concurrent.duration._
@ -186,7 +185,8 @@ class BaseServerTest extends JsonRpcServerTestKit {
stdOutController,
stdErrController,
stdInController,
runtimeConnectorProbe.ref
runtimeConnectorProbe.ref,
config
)
}

View File

@ -0,0 +1,97 @@
package org.enso.languageserver.websocket.json
import buildinfo.Info
import io.circe.literal.JsonStringContext
import org.enso.languageserver.data.Config
import org.enso.testkit.FlakySpec
import java.io.{File, FileOutputStream}
class WorkspaceOperationsTest extends BaseServerTest with FlakySpec {
"workspace/projectInfo" must {
val packageConfigName = Config.ensoPackageConfigName
val testYamlPath = new File(testContentRoot.toFile, packageConfigName)
"return the project info" taggedAs Flaky in {
testYamlPath.delete()
val packageYamlContents =
getClass.getClassLoader.getResourceAsStream(packageConfigName)
val yamlOutStream = new FileOutputStream(testYamlPath)
yamlOutStream.write(packageYamlContents.readAllBytes())
yamlOutStream.close()
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "workspace/projectInfo",
"id": 1,
"params": {}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 1,
"result": {
"projectName" : "Standard",
"engineVersion" : ${Info.ensoVersion},
"graalVersion" : ${Info.graalVersion}
}
}
""")
}
"return an error when the project configuration cannot be decoded" in {
testYamlPath.delete()
val yamlOutStream = new FileOutputStream(testYamlPath)
yamlOutStream.write(0x00)
yamlOutStream.close()
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "workspace/projectInfo",
"id": 1,
"params": {}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 1,
"error": {
"code": 1010,
"message": "Cannot decode the project configuration"
}
}
""")
}
"return an error when the project configuration is not present" in {
testYamlPath.delete()
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "workspace/projectInfo",
"id": 1,
"params": {}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 1,
"error": {
"code": 1003,
"message": "File not found"
}
}
""")
}
}
}

View File

@ -6,8 +6,10 @@ import nl.gn0s1s.bump.SemVer
*
* @param engineVersion the version of the started language server
* @param sockets the sockets listened by the language server
* @param projectName the name of the project
*/
case class RunningLanguageServerInfo(
engineVersion: SemVer,
sockets: LanguageServerSockets
sockets: LanguageServerSockets,
projectName: String
)

View File

@ -76,7 +76,8 @@ object ProjectManagementApi {
case class Result(
engineVersion: SemVer,
languageServerJsonAddress: Socket,
languageServerBinaryAddress: Socket
languageServerBinaryAddress: Socket,
projectName: String
)
implicit val hasParams = new HasParams[this.type] {

View File

@ -54,7 +54,8 @@ class ProjectOpenHandler[F[+_, +_]: Exec: CovariantFlatMap](
} yield ProjectOpen.Result(
engineVersion = server.engineVersion,
languageServerJsonAddress = server.sockets.jsonSocket,
languageServerBinaryAddress = server.sockets.binarySocket
languageServerBinaryAddress = server.sockets.binarySocket,
projectName = server.projectName
)
}

View File

@ -311,7 +311,7 @@ class ProjectService[
s"Language server boot failed. ${th.getMessage}"
)
}
} yield RunningLanguageServerInfo(version, sockets)
} yield RunningLanguageServerInfo(version, sockets, project.name)
/** @inheritdoc */
override def closeProject(

View File

@ -1,7 +1,6 @@
package org.enso.projectmanager
import java.util.UUID
import akka.testkit.TestDuration
import io.circe.Json
import io.circe.syntax._
@ -10,6 +9,7 @@ import org.enso.pkg.SemVerJson._
import io.circe.parser.parse
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.{MissingComponentAction, Socket}
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectOpen
import scala.concurrent.duration._
@ -70,6 +70,26 @@ trait ProjectManagementOps { this: BaseServerSpec =>
socket.fold(fail(s"Failed to decode json: $openReply", _), identity)
}
def openProjectData(implicit client: WsTestClient): ProjectOpen.Result = {
val Right(openReply) = parse(client.expectMessage(20.seconds.dilated))
val openResult = for {
result <- openReply.hcursor.downExpectedField("result")
engineVer <- result.downField("engineVersion").as[SemVer]
jsonAddr <- result.downExpectedField("languageServerJsonAddress")
jsonHost <- jsonAddr.downField("host").as[String]
jsonPort <- jsonAddr.downField("port").as[Int]
binAddr <- result.downExpectedField("languageServerBinaryAddress")
binHost <- binAddr.downField("host").as[String]
binPort <- binAddr.downField("port").as[Int]
name <- result.downField("projectName").as[String]
} yield {
val jsonSock = Socket(jsonHost, jsonPort)
val binSock = Socket(binHost, binPort)
ProjectOpen.Result(engineVer, jsonSock, binSock, name)
}
openResult.getOrElse(throw new Exception("Should have worked."))
}
def closeProject(
projectId: UUID
)(implicit client: WsTestClient): Unit = {

View File

@ -303,6 +303,26 @@ class ProjectManagementApiSpec
"project/open" must {
"open a project" taggedAs Flaky in {
val projectName = "Test_Project"
implicit val client: WsTestClient = new WsTestClient(address)
val projectId = createProject(projectName)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 0,
"params": {
"projectId": $projectId
}
}
""")
val result = openProjectData
result.projectName shouldEqual projectName
result.engineVersion shouldEqual SemVer("0.0.1").get
closeProject(projectId)
deleteProject(projectId)
}
"fail when project doesn't exist" in {
val client = new WsTestClient(address)
client.send(json"""

View File

@ -1,3 +1,3 @@
F22125736BE5D0452B568F45CE4A76EBEF27F339469B9DD3F5476B43369DBEB3
99F30FC4FA243330ADBC746BDB8F5B7CF2DD82FB7C4E023F1ED29B231395B46A
DBB6898C0166DF679A4179FFFADD4CD98C48D42AF072CA168FED27B41E1C3E43
0