mirror of
https://github.com/enso-org/enso.git
synced 2024-11-25 21:25:20 +03:00
Integration with the Searcher Database (#994)
This commit is contained in:
parent
2801f58ba9
commit
30d136a141
@ -622,8 +622,8 @@ lazy val testkit = project
|
||||
.in(file("lib/scala/testkit"))
|
||||
.settings(
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion
|
||||
)
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion
|
||||
)
|
||||
)
|
||||
|
||||
lazy val `core-definition` = (project in file("lib/scala/core-definition"))
|
||||
@ -674,6 +674,7 @@ lazy val searcher = project
|
||||
)
|
||||
.dependsOn(testkit % Test)
|
||||
.settings(licenseSettings)
|
||||
.dependsOn(`polyglot-api`)
|
||||
|
||||
lazy val `interpreter-dsl` = (project in file("lib/scala/interpreter-dsl"))
|
||||
.settings(
|
||||
@ -728,7 +729,6 @@ lazy val `polyglot-api` = project
|
||||
.settings(licenseSettings)
|
||||
.dependsOn(pkg)
|
||||
.dependsOn(`text-buffer`)
|
||||
.dependsOn(`searcher`)
|
||||
|
||||
lazy val `language-server` = (project in file("engine/language-server"))
|
||||
.settings(
|
||||
|
@ -265,10 +265,10 @@ The language construct that can be returned as a suggestion.
|
||||
// The definition scope
|
||||
interface SuggestionEntryScope {
|
||||
|
||||
// The start of the definition scope
|
||||
start: number;
|
||||
// The end of the definition scope
|
||||
end: number;
|
||||
// The start position of the definition scope
|
||||
start: Position;
|
||||
// The end position of the definition scope
|
||||
end: Position;
|
||||
}
|
||||
|
||||
// A type of suggestion entries.
|
||||
@ -330,6 +330,20 @@ type SuggestionEntryType
|
||||
| Local;
|
||||
```
|
||||
|
||||
### `SuggestionsDatabaseEntry`
|
||||
|
||||
#### Format
|
||||
The entry in the suggestions database.
|
||||
|
||||
``` typescript
|
||||
interface SuggestionsDatabaseEntry {
|
||||
// suggestion entry id
|
||||
id: number;
|
||||
// suggestion entry
|
||||
suggestion: SuggestionEntry;
|
||||
}
|
||||
```
|
||||
|
||||
### `SuggestionsDatabaseUpdate`
|
||||
The update of the suggestions database.
|
||||
|
||||
@ -340,6 +354,7 @@ The update of the suggestions database.
|
||||
type SuggestionsDatabaseUpdate
|
||||
= Add
|
||||
| Remove
|
||||
| Modify
|
||||
|
||||
interface Add {
|
||||
// suggestion entry id
|
||||
@ -351,6 +366,14 @@ interface Add {
|
||||
interface Remove {
|
||||
// suggestion entry id
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Modify {
|
||||
// suggestion entry id
|
||||
id: number;
|
||||
// new return type
|
||||
returnType: String;
|
||||
}
|
||||
```
|
||||
|
||||
### `File`
|
||||
@ -2465,14 +2488,17 @@ null
|
||||
```typescript
|
||||
{
|
||||
// The list of suggestions database entries
|
||||
entries: [SuggestionsDatabaseUpdate];
|
||||
entries: [SuggestionsDatabaseEntry];
|
||||
// The version of received suggestions database
|
||||
currentVersion: number;
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
TBC
|
||||
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror) an error accessing the
|
||||
suggestions database
|
||||
- [`ProjectNotFoundError`](#projectnotfounderror) project is not found in the
|
||||
root directory
|
||||
|
||||
### `search/getSuggestionsDatabaseVersion`
|
||||
Sent from client to the server to receive the current version of the suggestions
|
||||
@ -2497,7 +2523,10 @@ null
|
||||
```
|
||||
|
||||
#### Errors
|
||||
TBC
|
||||
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror) an error accessing the
|
||||
suggestions database
|
||||
- [`ProjectNotFoundError`](#projectnotfounderror) project is not found in the
|
||||
root directory
|
||||
|
||||
### `search/suggestionsDatabaseUpdate`
|
||||
Sent from server to the client to inform abouth the change in the suggestions
|
||||
@ -2518,7 +2547,7 @@ database.
|
||||
```
|
||||
|
||||
#### Errors
|
||||
TBC
|
||||
None
|
||||
|
||||
### `search/completion`
|
||||
Sent from client to the server to receive the autocomplete suggestion.
|
||||
@ -2554,7 +2583,12 @@ Sent from client to the server to receive the autocomplete suggestion.
|
||||
```
|
||||
|
||||
#### Errors
|
||||
TBC
|
||||
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror) an error accessing the
|
||||
suggestions database
|
||||
- [`ProjectNotFoundError`](#projectnotfounderror) project is not found in the
|
||||
root directory
|
||||
- [`ModuleNameNotResolvedError`](#modulenamenotresolvederror) the module name
|
||||
cannot be extracted from the provided file path parameter
|
||||
|
||||
## Input/Output Operations
|
||||
The input/output portion of the language server API deals with redirecting
|
||||
@ -2964,3 +2998,23 @@ Signals about an error accessing the suggestions database.
|
||||
"message" : "Suggestions database error"
|
||||
}
|
||||
```
|
||||
|
||||
### `ProjectNotFoundError`
|
||||
Signals that the project not found in the root directory.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 7002,
|
||||
"message" : "Project not found in the root directory"
|
||||
}
|
||||
```
|
||||
|
||||
### `ModuleNameNotResolvedError`
|
||||
Signals that the module name can not be resolved for the given file.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 7003,
|
||||
"message" : "Module name can't be resolved for the given file"
|
||||
}
|
||||
```
|
||||
|
@ -33,31 +33,57 @@ Server Message Specification](../language-server/protocol-language-server.md)
|
||||
document.
|
||||
|
||||
### Database Structure
|
||||
|
||||
Implementation utilizes the SQLite database.
|
||||
|
||||
Database is created per project and the database file stored in the project
|
||||
directory. That way the index can be preserved between the IDE restarts.
|
||||
|
||||
#### Suggestions Table
|
||||
Suggestions table stores suggestion entries.
|
||||
|
||||
* `id` `INTEGER` - unique identifier
|
||||
* `kind` `INTEGER` - type of suggestion entry, i.e. Atom, Method, Function, or
|
||||
Local
|
||||
* `name` `TEXT` - entry name
|
||||
* `self_type` `TEXT` - self type of the Method
|
||||
* `return_type` `TEXT` - return type of the entry
|
||||
* `documentation` `TEXT` - documentation string
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `INTEGER` | the unique identifier |
|
||||
| `externalId` | `UUID` | the external id from the IR |
|
||||
| `kind` | `INTEGER` | the type of suggestion entry, i.e. Atom, Method, Function, or Local |
|
||||
| `module` | `TEXT` | the module name |
|
||||
| `name` | `TEXT` | the suggestion name |
|
||||
| `self_type` | `TEXT` | the self type of the Method |
|
||||
| `return_type` | `TEXT` | the return type of the entry |
|
||||
| `scope_start` | `INTEGER` | the start position of the definition scope |
|
||||
| `scope_end` | `INTEGER` | the end position of the definition scope |
|
||||
| `documentation` | `TEXT` | the documentation string |
|
||||
|
||||
#### Arguments Table
|
||||
Arguments table stores all suggestion arguments with `suggestion_id` foreign
|
||||
key.
|
||||
|
||||
* `id` `INTEGER` - unique identifier
|
||||
* `suggestion_id` `INTEGER` - suggestion key this argument relates to
|
||||
* `name` `TEXT` - argument name
|
||||
* `type` `TEXT` - argument type; const 'Any' is used to specify generic types
|
||||
* `is_suspended` `INTEGER` - indicates whether the argument is lazy
|
||||
* `has_defult` `INTEGER` - indicates whether the argument has default value
|
||||
* `default_value` `TEXT` - optional default value
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `INTEGER` | the unique identifier |
|
||||
| `suggestion_id` | `INTEGER` | the suggestion key this argument relates to |
|
||||
| `index` | `INTEGER` | the argument position in the arguments list |
|
||||
| `name` | `TEXT` | the argument name |
|
||||
| `type` | `TEXT` | the argument type; const 'Any' is used to specify generic types |
|
||||
| `is_suspended` | `INTEGER` | indicates whether the argument is lazy |
|
||||
| `has_defult` | `INTEGER` | indicates whether the argument has default value |
|
||||
| `default_value` | `TEXT` | the optional default value |
|
||||
|
||||
#### Suggestions Version Table
|
||||
Versions table has a single row with the current database version. The version
|
||||
is updated on every change in the suggestions table.
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `INTEGER` | the unique identifier representing the currend database version |
|
||||
|
||||
#### File Versions Table
|
||||
Keeps track of SHA versions of the opened files.
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `path` | `TEXT` | the unique identifier of the file |
|
||||
| `digest` | `BLOB` | the SHA hash of the file contents |
|
||||
|
||||
### Static Analysis
|
||||
The database is filled by analyzing the Intermediate Representation (`IR`)
|
||||
@ -113,6 +139,94 @@ For example, when completing the argument of `calculate: Number -> Number`
|
||||
function, the search results will have the order of: `x2` > `x1` > `const_x`.
|
||||
|
||||
### Type
|
||||
|
||||
Suggestions based on the type are selected by matching with the string runtime
|
||||
type representation.
|
||||
|
||||
## Implementation
|
||||
The searcher primarily consists of:
|
||||
|
||||
- Suggestions database that stores suggestion entries and SHA hashes of the
|
||||
opened file contents.
|
||||
- Search requests handler that serves search and capability requests, and
|
||||
listens to the notifications from the runtime.
|
||||
- Suggestion builder (aka _indexer_) that extracts suggestion entries from `IR`
|
||||
in compile time.
|
||||
|
||||
```
|
||||
|
||||
+--------------------------------+
|
||||
| SuggestionsDB |
|
||||
+-----------------+--------------+
|
||||
| SuggestionsRepo | VersionsRepo |
|
||||
+----+------------+---------+----+
|
||||
^ ^
|
||||
| |
|
||||
Capability,Search v v
|
||||
+--------+ Request +---------+----------+ +--+------------------+
|
||||
| Client +<---------------->+ SuggestionsHandler | | CollaborativeBuffer |
|
||||
+--------+ Database Update +---------+----------+ +-------------------+-+
|
||||
Notifications ^ OpenFileNotification |
|
||||
| (isIndexed) |
|
||||
| v
|
||||
+---------+---------------------------------------+-------+
|
||||
| RuntimeConnector |
|
||||
+----+----------+---------------------------------+-------+
|
||||
^ ^ |
|
||||
ExpressionValuesComputed | | SuggestionsDBUpdate |
|
||||
| | |
|
||||
+------------------+--+ +--+----------------+ |
|
||||
| ExecutionInstrument | | EnsureCompiledJob | +----+------+
|
||||
+---------------------+ +-------------------+ | Module |
|
||||
| | (re)index +-----------+
|
||||
| SuggestionBuilder +-----------+ isIndexed |
|
||||
| | +-----------+
|
||||
+-------------------+
|
||||
|
||||
```
|
||||
|
||||
### Indexing
|
||||
Indexing is a process of extracting suggestions information from the `IR`. To
|
||||
keep the index in the consistent state, the language server tracks SHA versions
|
||||
of all opened files in the suggestions database.
|
||||
|
||||
- When the user opens a file, we compare its SHA hash with the saved digest. If
|
||||
they don't match, we mark the file for re-index by setting the corresponding
|
||||
flag in the OpenFile notification sent to the runtime. On receive, the runtime
|
||||
marks the module as not indexed.
|
||||
- When the file is compiled, we check the indexed flag on the module. If the
|
||||
module has not been indexed yet, we send all suggestions extracted from the IR
|
||||
as a ReIndexed update to the language server. If the module has been indexed,
|
||||
we only send suggestions that have been affected by the recent edits as a
|
||||
Database update.
|
||||
- When the language server receives ReIndexed update, it removes all suggestions
|
||||
related to this module, applies newly received updates, and sends a combined
|
||||
update of removed and added entries to the subscribed users. When the language
|
||||
server receives a regular Database update, it applies the update on the
|
||||
database and sends the appropriate updates to the subscribers.
|
||||
- When the user deletes a file, we remove all entries with the corresponding
|
||||
module from the database and update the users.
|
||||
|
||||
### Suggestion Builder
|
||||
The suggestion builder takes part in the indexing process. It extracts
|
||||
suggestions from the compiled `IR`. It also converts `IR` locations represented
|
||||
as an absolute char indexes (from the beginning of the file) to a position
|
||||
relative to a line used in the rest of the project.
|
||||
|
||||
### Runtime Instrumentation
|
||||
Apart from the type ascriptions, we can only get the return types of expressions
|
||||
in runtime. On the module execution, the language server listens to the
|
||||
`ExpressionValuesComputed` notifications sent from the runtime, which contain
|
||||
the external id and the actual return type. Then the language server uses
|
||||
external id as a key to update return types in the database.
|
||||
|
||||
### Search Requests
|
||||
The search request handler has direct access to the database. The completion
|
||||
request is more complicated then others. To query the database, it needs to
|
||||
convert the requested file path to the module name. To do this, on startup, the
|
||||
language server loads the package definition from the root directory and obtains
|
||||
the project name. Then, having the project name and the root directory path, it
|
||||
can recover the module name from the requested file path.
|
||||
|
||||
The project name can be changed with the refactoring `renameProject` command. To
|
||||
cover this case, the request handler listens to the `ProjectNameChanged` event
|
||||
and updates the project name accordingly.
|
||||
|
@ -32,14 +32,15 @@ class LanguageServerComponent(config: LanguageServerConfig)
|
||||
|
||||
implicit private val ec = config.computeExecutionContext
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def start(): Future[ComponentStarted.type] = {
|
||||
logger.info("Starting Language Server...")
|
||||
for {
|
||||
module <- Future { new MainModule(config) }
|
||||
jsonBinding <- module.jsonRpcServer.bind(config.interface, config.rpcPort)
|
||||
binaryBinding <- module.binaryServer
|
||||
.bind(config.interface, config.dataPort)
|
||||
binaryBinding <-
|
||||
module.binaryServer
|
||||
.bind(config.interface, config.dataPort)
|
||||
_ <- Future {
|
||||
maybeServerCtx = Some(ServerContext(module, jsonBinding, binaryBinding))
|
||||
}
|
||||
@ -52,7 +53,7 @@ class LanguageServerComponent(config: LanguageServerConfig)
|
||||
} yield ComponentStarted
|
||||
}
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def stop(): Future[ComponentStopped.type] =
|
||||
maybeServerCtx match {
|
||||
case None =>
|
||||
@ -62,22 +63,30 @@ class LanguageServerComponent(config: LanguageServerConfig)
|
||||
for {
|
||||
_ <- terminateTruffle(serverContext)
|
||||
_ <- terminateAkka(serverContext)
|
||||
_ <- releaseResources(serverContext)
|
||||
_ <- Future { maybeServerCtx = None }
|
||||
} yield ComponentStopped
|
||||
}
|
||||
|
||||
private def releaseResources(serverContext: ServerContext): Future[Unit] =
|
||||
for {
|
||||
_ <- Future(serverContext.mainModule.close()).recover(logError)
|
||||
_ <- Future { logger.info("Terminated main module") }
|
||||
} yield ()
|
||||
|
||||
private def terminateAkka(serverContext: ServerContext): Future[Unit] = {
|
||||
for {
|
||||
_ <- serverContext.jsonBinding.terminate(2.seconds).recover(logError)
|
||||
_ <- Future { logger.info("Terminated json connections") }
|
||||
_ <- serverContext.binaryBinding.terminate(2.seconds).recover(logError)
|
||||
_ <- Future { logger.info("Terminated binary connections") }
|
||||
_ <- Await
|
||||
.ready(
|
||||
serverContext.mainModule.system.terminate().recover(logError),
|
||||
2.seconds
|
||||
)
|
||||
.recover(logError)
|
||||
_ <-
|
||||
Await
|
||||
.ready(
|
||||
serverContext.mainModule.system.terminate().recover(logError),
|
||||
2.seconds
|
||||
)
|
||||
.recover(logError)
|
||||
_ <- Future { logger.info("Terminated actor system") }
|
||||
} yield ()
|
||||
}
|
||||
@ -94,7 +103,7 @@ class LanguageServerComponent(config: LanguageServerConfig)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def restart(): Future[ComponentRestarted.type] =
|
||||
for {
|
||||
_ <- stop()
|
||||
|
@ -29,7 +29,7 @@ import org.enso.languageserver.session.SessionRouter
|
||||
import org.enso.languageserver.text.BufferRegistry
|
||||
import org.enso.languageserver.util.binary.BinaryEncoder
|
||||
import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo}
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
import org.enso.searcher.sql.{SqlSuggestionsRepo, SqlVersionsRepo}
|
||||
import org.graalvm.polyglot.Context
|
||||
import org.graalvm.polyglot.io.MessageEndpoint
|
||||
|
||||
@ -46,7 +46,8 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
Map(serverConfig.contentRootUuid -> new File(serverConfig.contentRootPath)),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig()
|
||||
ExecutionContextConfig(),
|
||||
DirectoriesConfig(serverConfig.contentRootPath)
|
||||
)
|
||||
|
||||
val zioExec = ZioExec(zio.Runtime.default)
|
||||
@ -67,7 +68,17 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
implicit val materializer = SystemMaterializer.get(system)
|
||||
|
||||
val suggestionsRepo = {
|
||||
val repo = SqlSuggestionsRepo()(system.dispatcher)
|
||||
val repo = SqlSuggestionsRepo(
|
||||
languageServerConfig.directories.suggestionsDatabaseFile
|
||||
)(system.dispatcher)
|
||||
repo.init
|
||||
repo
|
||||
}
|
||||
|
||||
val versionsRepo = {
|
||||
val repo = SqlVersionsRepo(
|
||||
languageServerConfig.directories.suggestionsDatabaseFile
|
||||
)(system.dispatcher)
|
||||
repo.init
|
||||
repo
|
||||
}
|
||||
@ -85,7 +96,7 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
|
||||
lazy val bufferRegistry =
|
||||
system.actorOf(
|
||||
BufferRegistry.props(fileManager, runtimeConnector),
|
||||
BufferRegistry.props(versionsRepo, fileManager, runtimeConnector),
|
||||
"buffer-registry"
|
||||
)
|
||||
|
||||
@ -96,20 +107,19 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
"file-event-registry"
|
||||
)
|
||||
|
||||
lazy val suggestionsDatabaseEventsListener =
|
||||
system.actorOf(
|
||||
SuggestionsDatabaseEventsListener.props(sessionRouter, suggestionsRepo)
|
||||
)
|
||||
|
||||
lazy val suggestionsHandler =
|
||||
system.actorOf(SuggestionsHandler.props(suggestionsRepo))
|
||||
system.actorOf(
|
||||
SuggestionsHandler
|
||||
.props(languageServerConfig, suggestionsRepo, sessionRouter),
|
||||
"suggestions-handler"
|
||||
)
|
||||
|
||||
lazy val capabilityRouter =
|
||||
system.actorOf(
|
||||
CapabilityRouter.props(
|
||||
bufferRegistry,
|
||||
receivesTreeUpdatesHandler,
|
||||
suggestionsDatabaseEventsListener
|
||||
suggestionsHandler
|
||||
),
|
||||
"capability-router"
|
||||
)
|
||||
@ -205,4 +215,9 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
new BinaryConnectionControllerFactory(fileManager)
|
||||
)
|
||||
|
||||
/** Close the main module releasing all resources. */
|
||||
def close(): Unit = {
|
||||
suggestionsRepo.close()
|
||||
versionsRepo.close()
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,13 @@ import org.enso.languageserver.util.UnhandledLogging
|
||||
* @param bufferRegistry the recipient of buffer capability requests
|
||||
* @param receivesTreeUpdatesHandler the recipient of
|
||||
* `receivesTreeUpdates` capability requests
|
||||
* @param suggestionsDatabaseEventsListener the recipient of
|
||||
* @param suggestionsHandler the recipient of
|
||||
* `receivesSuggestionsDatabaseUpdates` capability requests
|
||||
*/
|
||||
class CapabilityRouter(
|
||||
bufferRegistry: ActorRef,
|
||||
receivesTreeUpdatesHandler: ActorRef,
|
||||
suggestionsDatabaseEventsListener: ActorRef
|
||||
suggestionsHandler: ActorRef
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
@ -58,13 +58,13 @@ class CapabilityRouter(
|
||||
_,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
suggestionsDatabaseEventsListener.forward(msg)
|
||||
suggestionsHandler.forward(msg)
|
||||
|
||||
case msg @ ReleaseCapability(
|
||||
_,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
suggestionsDatabaseEventsListener.forward(msg)
|
||||
suggestionsHandler.forward(msg)
|
||||
}
|
||||
|
||||
}
|
||||
@ -77,20 +77,20 @@ object CapabilityRouter {
|
||||
* @param bufferRegistry a buffer registry ref
|
||||
* @param receivesTreeUpdatesHandler the recipient of `receivesTreeUpdates`
|
||||
* capability requests
|
||||
* @param suggestionsDatabaseEventsListener the recipient of
|
||||
* @param suggestionsHandler the recipient of
|
||||
* `receivesSuggestionsDatabaseUpdates` capability requests
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(
|
||||
bufferRegistry: ActorRef,
|
||||
receivesTreeUpdatesHandler: ActorRef,
|
||||
suggestionsDatabaseEventsListener: ActorRef
|
||||
suggestionsHandler: ActorRef
|
||||
): Props =
|
||||
Props(
|
||||
new CapabilityRouter(
|
||||
bufferRegistry,
|
||||
receivesTreeUpdatesHandler,
|
||||
suggestionsDatabaseEventsListener
|
||||
suggestionsHandler
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.languageserver.data
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.languageserver.filemanager.{
|
||||
@ -10,6 +11,7 @@ import org.enso.languageserver.filemanager.{
|
||||
}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Configuration of the path watcher.
|
||||
@ -56,7 +58,7 @@ object FileManagerConfig {
|
||||
def apply(timeout: FiniteDuration): FileManagerConfig =
|
||||
FileManagerConfig(
|
||||
timeout = timeout,
|
||||
parallelism = Runtime.getRuntime().availableProcessors()
|
||||
parallelism = Runtime.getRuntime.availableProcessors()
|
||||
)
|
||||
}
|
||||
|
||||
@ -78,17 +80,55 @@ object ExecutionContextConfig {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration of directories for storing internal files.
|
||||
*
|
||||
* @param root the root directory path
|
||||
*/
|
||||
case class DirectoriesConfig(root: File) {
|
||||
|
||||
/** The data directory path. */
|
||||
val dataDirectory: File =
|
||||
new File(root, DirectoriesConfig.DataDirectory)
|
||||
|
||||
/** The suggestions database file path. */
|
||||
val suggestionsDatabaseFile: File =
|
||||
new File(dataDirectory, DirectoriesConfig.SuggestionsDatabaseFile)
|
||||
|
||||
Try(Files.createDirectories(dataDirectory.toPath))
|
||||
}
|
||||
|
||||
object DirectoriesConfig {
|
||||
|
||||
val DataDirectory: String = ".enso"
|
||||
val SuggestionsDatabaseFile: String = "suggestions.db"
|
||||
|
||||
/**
|
||||
* Create default data directory config
|
||||
*
|
||||
* @param root the root directory path
|
||||
* @return default data directory config
|
||||
*/
|
||||
def apply(root: String): DirectoriesConfig =
|
||||
new DirectoriesConfig(new File(root))
|
||||
}
|
||||
|
||||
/**
|
||||
* The config of the running Language Server instance.
|
||||
*
|
||||
* @param contentRoots a mapping between content root id and absolute path to
|
||||
* the content root
|
||||
* the content root
|
||||
* @param fileManager the file manater 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, File],
|
||||
fileManager: FileManagerConfig,
|
||||
pathWatcher: PathWatcherConfig,
|
||||
executionContext: ExecutionContextConfig
|
||||
executionContext: ExecutionContextConfig,
|
||||
directories: DirectoriesConfig
|
||||
) {
|
||||
|
||||
def findContentRoot(rootId: UUID): Either[FileSystemFailure, File] =
|
||||
|
@ -9,8 +9,16 @@ trait ContentBasedVersioning {
|
||||
* Evaluates content-based version of document.
|
||||
*
|
||||
* @param content a textual content
|
||||
* @return a digest
|
||||
* @return a string representation of a digest
|
||||
*/
|
||||
def evalVersion(content: String): String
|
||||
|
||||
/**
|
||||
* Evaluates content-based version of document.
|
||||
*
|
||||
* @param content a textual content
|
||||
* @return a digest
|
||||
*/
|
||||
def evalDigest(content: String): Array[Byte]
|
||||
|
||||
}
|
||||
|
@ -8,16 +8,14 @@ import org.bouncycastle.util.encoders.Hex
|
||||
*/
|
||||
object Sha3_224VersionCalculator extends ContentBasedVersioning {
|
||||
|
||||
/**
|
||||
* Digests textual content.
|
||||
*
|
||||
* @param content a textual content
|
||||
* @return a digest
|
||||
*/
|
||||
override def evalVersion(content: String): String = {
|
||||
/** @inheritdoc */
|
||||
override def evalVersion(content: String): String =
|
||||
Hex.toHexString(evalDigest(content))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def evalDigest(content: String): Array[Byte] = {
|
||||
val digestSHA3 = new SHA3.Digest224()
|
||||
val hash = digestSHA3.digest(content.getBytes("UTF-8"))
|
||||
Hex.toHexString(hash)
|
||||
digestSHA3.digest(content.getBytes("UTF-8"))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package org.enso.languageserver.filemanager
|
||||
|
||||
import org.enso.languageserver.event.Event
|
||||
|
||||
/**
|
||||
* A notification about successful file deletion action
|
||||
*
|
||||
* @param file the deleted file
|
||||
*/
|
||||
case class FileDeletedEvent(file: Path) extends Event
|
@ -55,7 +55,7 @@ object FileEventKind extends Enum[FileEventKind] with CirceEnum[FileEventKind] {
|
||||
*/
|
||||
case object Modified extends FileEventKind
|
||||
|
||||
val values = findValues
|
||||
override val values = findValues
|
||||
|
||||
/**
|
||||
* Create [[FileEventKind]] from [[WatcherAdapter.EventType]].
|
||||
|
@ -171,17 +171,17 @@ final class PathWatcher(
|
||||
}
|
||||
.leftMap(errorHandler)
|
||||
|
||||
private val errorHandler: Throwable => FileSystemFailure = {
|
||||
case ex => GenericFileSystemFailure(ex.getMessage)
|
||||
private val errorHandler: Throwable => FileSystemFailure = { ex =>
|
||||
GenericFileSystemFailure(ex.getMessage)
|
||||
}
|
||||
}
|
||||
|
||||
object PathWatcher {
|
||||
|
||||
/**
|
||||
* Conunt unsuccessful file watcher restarts
|
||||
* Counter for unsuccessful file watcher restarts.
|
||||
*
|
||||
* @param maxRestarts maximum number of restarts we can try
|
||||
* @param maxRestarts maximum restart attempts
|
||||
*/
|
||||
final private class RestartCounter(maxRestarts: Int) {
|
||||
|
||||
|
@ -185,8 +185,8 @@ class JsonConnectionController(
|
||||
)
|
||||
|
||||
case SearchProtocol.SuggestionsDatabaseUpdateNotification(
|
||||
updates,
|
||||
version
|
||||
version,
|
||||
updates
|
||||
) =>
|
||||
webActor ! Notification(
|
||||
SearchApi.SuggestionsDatabaseUpdates,
|
||||
|
@ -0,0 +1,10 @@
|
||||
package org.enso.languageserver.refactoring
|
||||
|
||||
import org.enso.languageserver.event.Event
|
||||
|
||||
/**
|
||||
* An event notifying that project name has changed.
|
||||
*
|
||||
* @param name the new project name
|
||||
*/
|
||||
case class ProjectNameChangedEvent(name: String) extends Event
|
@ -4,8 +4,10 @@ import akka.actor._
|
||||
import org.enso.jsonrpc.Errors.ServiceError
|
||||
import org.enso.jsonrpc._
|
||||
import org.enso.languageserver.filemanager.{
|
||||
FileDeletedEvent,
|
||||
FileManagerProtocol,
|
||||
FileSystemFailureMapper
|
||||
FileSystemFailureMapper,
|
||||
Path
|
||||
}
|
||||
import org.enso.languageserver.filemanager.FileManagerApi.DeleteFile
|
||||
import org.enso.languageserver.requesthandler.RequestTimeout
|
||||
@ -27,13 +29,14 @@ class DeleteFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef)
|
||||
fileManager ! FileManagerProtocol.DeleteFile(params.path)
|
||||
val cancellable = context.system.scheduler
|
||||
.scheduleOnce(requestTimeout, self, RequestTimeout)
|
||||
context.become(responseStage(id, sender(), cancellable))
|
||||
context.become(responseStage(id, sender(), cancellable, params.path))
|
||||
}
|
||||
|
||||
private def responseStage(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
cancellable: Cancellable
|
||||
cancellable: Cancellable,
|
||||
path: Path
|
||||
): Receive = {
|
||||
case Status.Failure(ex) =>
|
||||
log.error(s"Failure during $DeleteFile operation:", ex)
|
||||
@ -55,6 +58,7 @@ class DeleteFileHandler(requestTimeout: FiniteDuration, fileManager: ActorRef)
|
||||
context.stop(self)
|
||||
|
||||
case FileManagerProtocol.DeleteFileResult(Right(())) =>
|
||||
context.system.eventStream.publish(FileDeletedEvent(path))
|
||||
replyTo ! ResponseResult(DeleteFile, id, Unused)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
@ -5,6 +5,7 @@ import java.util.UUID
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props}
|
||||
import org.enso.jsonrpc.Errors.ServiceError
|
||||
import org.enso.jsonrpc._
|
||||
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
|
||||
import org.enso.languageserver.requesthandler.RequestTimeout
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
@ -46,7 +47,8 @@ class RenameProjectHandler(timeout: FiniteDuration, runtimeConnector: ActorRef)
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, Api.ProjectRenamed()) =>
|
||||
case Api.Response(_, Api.ProjectRenamed(name)) =>
|
||||
context.system.eventStream.publish(ProjectNameChangedEvent(name))
|
||||
replyTo ! ResponseResult(RenameProject, id, Unused)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
@ -8,7 +8,7 @@ import org.enso.languageserver.runtime.SearchApi.{
|
||||
Completion,
|
||||
SuggestionsDatabaseError
|
||||
}
|
||||
import org.enso.languageserver.runtime.SearchProtocol
|
||||
import org.enso.languageserver.runtime.{SearchFailureMapper, SearchProtocol}
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
@ -34,10 +34,10 @@ class CompletionHandler(
|
||||
case Request(
|
||||
Completion,
|
||||
id,
|
||||
Completion.Params(module, pos, selfType, returnType, tags)
|
||||
Completion.Params(file, pos, selfType, returnType, tags)
|
||||
) =>
|
||||
suggestionsHandler ! SearchProtocol.Completion(
|
||||
module,
|
||||
file,
|
||||
pos,
|
||||
selfType,
|
||||
returnType,
|
||||
@ -64,6 +64,9 @@ class CompletionHandler(
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case msg: SearchProtocol.SearchFailure =>
|
||||
replyTo ! ResponseError(Some(id), SearchFailureMapper.mapFailure(msg))
|
||||
|
||||
case SearchProtocol.CompletionResult(version, results) =>
|
||||
replyTo ! ResponseResult(
|
||||
Completion,
|
||||
|
@ -8,7 +8,7 @@ import org.enso.languageserver.runtime.SearchApi.{
|
||||
GetSuggestionsDatabase,
|
||||
SuggestionsDatabaseError
|
||||
}
|
||||
import org.enso.languageserver.runtime.SearchProtocol
|
||||
import org.enso.languageserver.runtime.{SearchFailureMapper, SearchProtocol}
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
@ -54,7 +54,10 @@ class GetSuggestionsDatabaseHandler(
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case SearchProtocol.GetSuggestionsDatabaseResult(updates, version) =>
|
||||
case msg: SearchProtocol.SearchFailure =>
|
||||
replyTo ! ResponseError(Some(id), SearchFailureMapper.mapFailure(msg))
|
||||
|
||||
case SearchProtocol.GetSuggestionsDatabaseResult(version, updates) =>
|
||||
replyTo ! ResponseResult(
|
||||
GetSuggestionsDatabase,
|
||||
id,
|
||||
|
@ -8,7 +8,7 @@ import org.enso.languageserver.runtime.SearchApi.{
|
||||
GetSuggestionsDatabaseVersion,
|
||||
SuggestionsDatabaseError
|
||||
}
|
||||
import org.enso.languageserver.runtime.SearchProtocol
|
||||
import org.enso.languageserver.runtime.{SearchFailureMapper, SearchProtocol}
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
@ -54,6 +54,9 @@ class GetSuggestionsDatabaseVersionHandler(
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case msg: SearchProtocol.SearchFailure =>
|
||||
replyTo ! ResponseError(Some(id), SearchFailureMapper.mapFailure(msg))
|
||||
|
||||
case SearchProtocol.GetSuggestionsDatabaseVersionResult(version) =>
|
||||
replyTo ! ResponseResult(
|
||||
GetSuggestionsDatabaseVersion,
|
||||
|
@ -0,0 +1,63 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
object ModuleNameBuilder {
|
||||
|
||||
/**
|
||||
* Build the module name from the file path.
|
||||
*
|
||||
* @param projectName the project name
|
||||
* @param root the project root directory
|
||||
* @param file the module file path
|
||||
* @return the module name
|
||||
*/
|
||||
def build(projectName: String, root: Path, file: Path): Option[String] = {
|
||||
getModuleSegments(root, file).map { modules =>
|
||||
toModule(projectName +: modules :+ getModuleName(file))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract segments related to the module from the file path.
|
||||
*
|
||||
* @param root the project root directory
|
||||
* @param file the module file path
|
||||
* @return the list of module segments
|
||||
*/
|
||||
private def getModuleSegments(
|
||||
root: Path,
|
||||
file: Path
|
||||
): Option[Vector[String]] = {
|
||||
Try(root.relativize(file)).toOption
|
||||
.map { relativePath =>
|
||||
val b = Vector.newBuilder[String]
|
||||
1.until(relativePath.getNameCount - 1)
|
||||
.foreach(i => b += relativePath.getName(i).toString)
|
||||
b.result()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module name from the file path.
|
||||
*
|
||||
* @param file the module file path
|
||||
* @return the module name
|
||||
*/
|
||||
private def getModuleName(file: Path): String = {
|
||||
val fileName = file.getFileName.toString
|
||||
fileName.substring(0, fileName.lastIndexOf('.'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the list of segments to a module name.
|
||||
*
|
||||
* @param segments the list of segments
|
||||
* @return the fully qualified module name
|
||||
*/
|
||||
private def toModule(segments: Iterable[String]): String =
|
||||
segments.mkString(".")
|
||||
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
|
||||
import org.enso.languageserver.filemanager.Path
|
||||
import org.enso.languageserver.runtime.SearchProtocol.{
|
||||
SuggestionId,
|
||||
SuggestionKind,
|
||||
SuggestionsDatabaseUpdate
|
||||
}
|
||||
import org.enso.searcher.SuggestionEntry
|
||||
import org.enso.text.editing.model.Position
|
||||
|
||||
/**
|
||||
@ -32,7 +34,7 @@ object SearchApi {
|
||||
extends Method("search/getSuggestionsDatabase") {
|
||||
|
||||
case class Result(
|
||||
entries: Seq[SuggestionsDatabaseUpdate],
|
||||
entries: Seq[SuggestionEntry],
|
||||
currentVersion: Long
|
||||
)
|
||||
|
||||
@ -47,7 +49,7 @@ object SearchApi {
|
||||
case object GetSuggestionsDatabaseVersion
|
||||
extends Method("search/getSuggestionsDatabaseVersion") {
|
||||
|
||||
case class Result(version: Long)
|
||||
case class Result(currentVersion: Long)
|
||||
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = Unused.type
|
||||
@ -60,7 +62,7 @@ object SearchApi {
|
||||
case object Completion extends Method("search/completion") {
|
||||
|
||||
case class Params(
|
||||
module: String,
|
||||
file: Path,
|
||||
position: Position,
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
@ -79,4 +81,10 @@ object SearchApi {
|
||||
|
||||
case object SuggestionsDatabaseError
|
||||
extends Error(7001, "Suggestions database error")
|
||||
|
||||
case object ProjectNotFoundError
|
||||
extends Error(7002, "Project not found in the root directory")
|
||||
|
||||
case object ModuleNameNotResolvedError
|
||||
extends Error(7003, "Module name can't be resolved for the given file")
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import org.enso.jsonrpc.Error
|
||||
import org.enso.languageserver.filemanager.FileSystemFailureMapper
|
||||
import org.enso.languageserver.runtime.SearchProtocol.{
|
||||
FileSystemError,
|
||||
ModuleNameNotResolvedError,
|
||||
ProjectNotFoundError,
|
||||
SearchFailure
|
||||
}
|
||||
|
||||
object SearchFailureMapper {
|
||||
|
||||
/**
|
||||
* Maps [[SearchFailure]] into JSON RPC error.
|
||||
*
|
||||
* @param searchError the search specific failure
|
||||
* @return JSON RPC error
|
||||
*/
|
||||
def mapFailure(searchError: SearchFailure): Error =
|
||||
searchError match {
|
||||
case FileSystemError(e) => FileSystemFailureMapper.mapFailure(e)
|
||||
case ProjectNotFoundError => SearchApi.ProjectNotFoundError
|
||||
case ModuleNameNotResolvedError(_) => SearchApi.ModuleNameNotResolvedError
|
||||
}
|
||||
|
||||
}
|
@ -4,7 +4,9 @@ import enumeratum._
|
||||
import io.circe.generic.auto._
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, Encoder, Json}
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.enso.languageserver.filemanager.{FileSystemFailure, Path}
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.searcher.SuggestionEntry
|
||||
import org.enso.text.editing.model.Position
|
||||
|
||||
object SearchProtocol {
|
||||
@ -28,6 +30,14 @@ object SearchProtocol {
|
||||
*/
|
||||
case class Remove(id: SuggestionId) extends SuggestionsDatabaseUpdate
|
||||
|
||||
/** Modify the database entry.
|
||||
*
|
||||
* @param id the suggestion id
|
||||
* @param returnType the new return type
|
||||
*/
|
||||
case class Modify(id: SuggestionId, returnType: String)
|
||||
extends SuggestionsDatabaseUpdate
|
||||
|
||||
private object CodecField {
|
||||
|
||||
val Type = "type"
|
||||
@ -38,6 +48,8 @@ object SearchProtocol {
|
||||
val Add = "Add"
|
||||
|
||||
val Remove = "Remove"
|
||||
|
||||
val Modify = "Modify"
|
||||
}
|
||||
|
||||
implicit val decoder: Decoder[SuggestionsDatabaseUpdate] =
|
||||
@ -48,6 +60,9 @@ object SearchProtocol {
|
||||
|
||||
case CodecType.Remove =>
|
||||
Decoder[SuggestionsDatabaseUpdate.Remove].tryDecode(cursor)
|
||||
|
||||
case CodecType.Modify =>
|
||||
Decoder[SuggestionsDatabaseUpdate.Modify].tryDecode(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +78,12 @@ object SearchProtocol {
|
||||
Encoder[SuggestionsDatabaseUpdate.Remove]
|
||||
.apply(remove)
|
||||
.deepMerge(Json.obj(CodecField.Type -> CodecType.Remove.asJson))
|
||||
|
||||
case modify: SuggestionsDatabaseUpdate.Modify =>
|
||||
Encoder[SuggestionsDatabaseUpdate.Modify]
|
||||
.apply(modify)
|
||||
.deepMerge(Json.obj(CodecField.Type -> CodecType.Modify.asJson))
|
||||
.dropNullValues
|
||||
}
|
||||
|
||||
private object SuggestionType {
|
||||
@ -174,12 +195,12 @@ object SearchProtocol {
|
||||
|
||||
/** A notification about changes in the suggestions database.
|
||||
*
|
||||
* @param updates the list of database updates
|
||||
* @param currentVersion current version of the suggestions database
|
||||
* @param updates the list of database updates
|
||||
*/
|
||||
case class SuggestionsDatabaseUpdateNotification(
|
||||
updates: Seq[SuggestionsDatabaseUpdate],
|
||||
currentVersion: Long
|
||||
currentVersion: Long,
|
||||
updates: Seq[SuggestionsDatabaseUpdate]
|
||||
)
|
||||
|
||||
/** The request to receive contents of the suggestions database. */
|
||||
@ -187,12 +208,12 @@ object SearchProtocol {
|
||||
|
||||
/** The reply to the [[GetSuggestionsDatabase]] request.
|
||||
*
|
||||
* @param entries the entries of the suggestion database
|
||||
* @param currentVersion current version of the suggestions database
|
||||
* @param entries the entries of the suggestion database
|
||||
*/
|
||||
case class GetSuggestionsDatabaseResult(
|
||||
entries: Seq[SuggestionsDatabaseUpdate],
|
||||
currentVersion: Long
|
||||
currentVersion: Long,
|
||||
entries: Seq[SuggestionEntry]
|
||||
)
|
||||
|
||||
/** The request to receive the current version of the suggestions database. */
|
||||
@ -206,14 +227,14 @@ object SearchProtocol {
|
||||
|
||||
/** The completion request.
|
||||
*
|
||||
* @param module the edited module
|
||||
* @param file the edited file
|
||||
* @param position the cursor position
|
||||
* @param selfType filter entries matching the self type
|
||||
* @param returnType filter entries matching the return type
|
||||
* @param tags filter entries by suggestion type
|
||||
*/
|
||||
case class Completion(
|
||||
module: String,
|
||||
file: Path,
|
||||
position: Position,
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
@ -227,4 +248,18 @@ object SearchProtocol {
|
||||
*/
|
||||
case class CompletionResult(currentVersion: Long, results: Seq[SuggestionId])
|
||||
|
||||
/** Base trait for search request errors. */
|
||||
sealed trait SearchFailure
|
||||
|
||||
/** Signals about file system error. */
|
||||
case class FileSystemError(e: FileSystemFailure) extends SearchFailure
|
||||
|
||||
/** Signals that the project not found in the root directory. */
|
||||
case object ProjectNotFoundError extends SearchFailure
|
||||
|
||||
/** Signals that the module name can not be resolved for the given file.
|
||||
*
|
||||
* @param file the file path
|
||||
*/
|
||||
case class ModuleNameNotResolvedError(file: Path) extends SearchFailure
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||
AcquireCapability,
|
||||
CapabilityAcquired,
|
||||
CapabilityReleased,
|
||||
ReleaseCapability
|
||||
}
|
||||
import org.enso.languageserver.data.{
|
||||
CapabilityRegistration,
|
||||
ClientId,
|
||||
ReceivesSuggestionsDatabaseUpdates
|
||||
}
|
||||
import org.enso.languageserver.runtime.SearchProtocol.{
|
||||
SuggestionsDatabaseUpdate,
|
||||
SuggestionsDatabaseUpdateNotification
|
||||
}
|
||||
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.{Suggestion, SuggestionsRepo}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Event listener listens event stream for the suggestion database
|
||||
* notifications from the runtime and sends updates to the client. The listener
|
||||
* is a singleton and created per context registry.
|
||||
*
|
||||
* @param sessionRouter the session router
|
||||
* @param repo the suggestions repo
|
||||
*/
|
||||
final class SuggestionsDatabaseEventsListener(
|
||||
sessionRouter: ActorRef,
|
||||
repo: SuggestionsRepo[Future]
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def preStart(): Unit = {
|
||||
context.system.eventStream
|
||||
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
|
||||
}
|
||||
|
||||
override def receive: Receive = withClients(Set())
|
||||
|
||||
private def withClients(clients: Set[ClientId]): Receive = {
|
||||
case AcquireCapability(
|
||||
client,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
sender() ! CapabilityAcquired
|
||||
context.become(withClients(clients + client.clientId))
|
||||
|
||||
case ReleaseCapability(
|
||||
client,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
sender() ! CapabilityReleased
|
||||
context.become(withClients(clients - client.clientId))
|
||||
|
||||
case msg: Api.SuggestionsDatabaseUpdateNotification =>
|
||||
applyDatabaseUpdates(msg)
|
||||
.onComplete {
|
||||
case Success(notification) =>
|
||||
if (notification.updates.nonEmpty) {
|
||||
clients.foreach { clientId =>
|
||||
sessionRouter ! DeliverToJsonController(clientId, notification)
|
||||
}
|
||||
}
|
||||
case Failure(ex) =>
|
||||
log.error(
|
||||
ex,
|
||||
"Error applying suggestion database updates: {}",
|
||||
msg.updates
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def applyDatabaseUpdates(
|
||||
msg: Api.SuggestionsDatabaseUpdateNotification
|
||||
): Future[SuggestionsDatabaseUpdateNotification] = {
|
||||
val (added, removed) = msg.updates
|
||||
.foldLeft((Seq[Suggestion](), Seq[Suggestion]())) {
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Add) =>
|
||||
(add :+ msg.suggestion, remove)
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Remove) =>
|
||||
(add, remove :+ msg.suggestion)
|
||||
}
|
||||
|
||||
for {
|
||||
(_, removedIds) <- repo.removeAll(removed)
|
||||
(version, addedIds) <- repo.insertAll(added)
|
||||
} yield {
|
||||
val updatesRemoved = removedIds.collect {
|
||||
case Some(id) => SuggestionsDatabaseUpdate.Remove(id)
|
||||
}
|
||||
val updatesAdded =
|
||||
(addedIds zip added).flatMap {
|
||||
case (Some(id), suggestion) =>
|
||||
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
|
||||
case (None, suggestion) =>
|
||||
log.error("failed to insert suggestion: {}", suggestion)
|
||||
None
|
||||
}
|
||||
SuggestionsDatabaseUpdateNotification(
|
||||
updatesRemoved ++ updatesAdded,
|
||||
version
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SuggestionsDatabaseEventsListener {
|
||||
|
||||
/**
|
||||
* Creates a configuration object used to create a
|
||||
* [[SuggestionsDatabaseEventsListener]].
|
||||
*
|
||||
* @param sessionRouter the session router
|
||||
* @param repo the suggestions repo
|
||||
*/
|
||||
def props(
|
||||
sessionRouter: ActorRef,
|
||||
repo: SuggestionsRepo[Future]
|
||||
): Props =
|
||||
Props(new SuggestionsDatabaseEventsListener(sessionRouter, repo))
|
||||
|
||||
}
|
@ -1,28 +1,171 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props}
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import akka.pattern.pipe
|
||||
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||
AcquireCapability,
|
||||
CapabilityAcquired,
|
||||
CapabilityReleased,
|
||||
ReleaseCapability
|
||||
}
|
||||
import org.enso.languageserver.data.{
|
||||
CapabilityRegistration,
|
||||
ClientId,
|
||||
Config,
|
||||
ReceivesSuggestionsDatabaseUpdates
|
||||
}
|
||||
import org.enso.languageserver.filemanager.{FileDeletedEvent, Path}
|
||||
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
|
||||
import org.enso.languageserver.runtime.SearchProtocol._
|
||||
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.searcher.{SuggestionEntry, SuggestionsRepo}
|
||||
import org.enso.pkg.PackageManager
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.SuggestionsRepo
|
||||
import org.enso.text.editing.model.Position
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* The handler of search requests.
|
||||
*
|
||||
* Handler initializes the database and responds to the search requests.
|
||||
*
|
||||
* == Implementation ==
|
||||
*
|
||||
* {{
|
||||
*
|
||||
* +--------------------+
|
||||
* | SuggestionsRepo |
|
||||
* +---------+----------+
|
||||
* ^
|
||||
* Capability,Search |
|
||||
* Request/Response v
|
||||
* +---------+<---------------->+---------+----------+
|
||||
* | Clients | | SuggestionsHandler |
|
||||
* +---------+<-----------------+---------+----------+
|
||||
* Database Update ^
|
||||
* Notifications |
|
||||
* |
|
||||
* +---------+----------+
|
||||
* | RuntimeConnector |
|
||||
* +----+----------+----+
|
||||
* ^ ^
|
||||
* SuggestionsDatabaseUpdate | | ExpressionValuesComputed
|
||||
* | |
|
||||
* +----------------+--+ +--+--------------------+
|
||||
* | EnsureCompiledJob | | IdExecutionInstrument |
|
||||
* +-------------------+ +-----------------------+
|
||||
*
|
||||
* }}
|
||||
*
|
||||
* @param config the server configuration
|
||||
* @param repo the suggestions repo
|
||||
* @param sessionRouter the session router
|
||||
*/
|
||||
final class SuggestionsHandler(repo: SuggestionsRepo[Future])
|
||||
extends Actor
|
||||
final class SuggestionsHandler(
|
||||
config: Config,
|
||||
repo: SuggestionsRepo[Future],
|
||||
sessionRouter: ActorRef
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def preStart(): Unit = {
|
||||
context.system.eventStream
|
||||
.subscribe(self, classOf[Api.ExpressionValuesComputed])
|
||||
context.system.eventStream
|
||||
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
|
||||
context.system.eventStream
|
||||
.subscribe(self, classOf[Api.SuggestionsDatabaseReIndexNotification])
|
||||
context.system.eventStream.subscribe(self, classOf[ProjectNameChangedEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[FileDeletedEvent])
|
||||
|
||||
config.contentRoots.foreach {
|
||||
case (_, contentRoot) =>
|
||||
PackageManager.Default
|
||||
.fromDirectory(contentRoot)
|
||||
.foreach(pkg => self ! ProjectNameChangedEvent(pkg.config.name))
|
||||
}
|
||||
}
|
||||
|
||||
override def receive: Receive = {
|
||||
case ProjectNameChangedEvent(name) =>
|
||||
context.become(initialized(name, Set()))
|
||||
|
||||
case msg =>
|
||||
log.warning("Unhandled message: {}", msg)
|
||||
sender() ! ProjectNotFoundError
|
||||
}
|
||||
|
||||
def initialized(projectName: String, clients: Set[ClientId]): Receive = {
|
||||
case AcquireCapability(
|
||||
client,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
sender() ! CapabilityAcquired
|
||||
context.become(initialized(projectName, clients + client.clientId))
|
||||
|
||||
case ReleaseCapability(
|
||||
client,
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
) =>
|
||||
sender() ! CapabilityReleased
|
||||
context.become(initialized(projectName, clients - client.clientId))
|
||||
|
||||
case msg: Api.SuggestionsDatabaseUpdateNotification =>
|
||||
applyDatabaseUpdates(msg)
|
||||
.onComplete {
|
||||
case Success(notification) =>
|
||||
if (notification.updates.nonEmpty) {
|
||||
clients.foreach { clientId =>
|
||||
sessionRouter ! DeliverToJsonController(clientId, notification)
|
||||
}
|
||||
}
|
||||
case Failure(ex) =>
|
||||
log.error(
|
||||
ex,
|
||||
"Error applying suggestion database updates: {}",
|
||||
msg.updates
|
||||
)
|
||||
}
|
||||
|
||||
case msg: Api.SuggestionsDatabaseReIndexNotification =>
|
||||
applyReIndexUpdates(msg.moduleName, msg.updates)
|
||||
.onComplete {
|
||||
case Success(notification) =>
|
||||
if (notification.updates.nonEmpty) {
|
||||
clients.foreach { clientId =>
|
||||
sessionRouter ! DeliverToJsonController(clientId, notification)
|
||||
}
|
||||
}
|
||||
case Failure(ex) =>
|
||||
log.error(
|
||||
ex,
|
||||
"Error applying suggestion re-index updates: {}",
|
||||
msg.updates
|
||||
)
|
||||
}
|
||||
|
||||
case Api.ExpressionValuesComputed(_, updates) =>
|
||||
val types = updates.flatMap(update =>
|
||||
update.expressionType.map(update.expressionId -> _)
|
||||
)
|
||||
repo
|
||||
.updateAll(types)
|
||||
.map {
|
||||
case (version, updatedIds) =>
|
||||
val updates = types.zip(updatedIds).collect {
|
||||
case ((_, typeValue), Some(suggestionId)) =>
|
||||
SuggestionsDatabaseUpdate.Modify(suggestionId, typeValue)
|
||||
}
|
||||
SuggestionsDatabaseUpdateNotification(version, updates)
|
||||
}
|
||||
|
||||
case GetSuggestionsDatabaseVersion =>
|
||||
repo.currentVersion
|
||||
.map(GetSuggestionsDatabaseVersionResult)
|
||||
@ -30,26 +173,164 @@ final class SuggestionsHandler(repo: SuggestionsRepo[Future])
|
||||
|
||||
case GetSuggestionsDatabase =>
|
||||
repo.getAll
|
||||
.map(Function.tupled(toGetSuggestionsDatabaseResult))
|
||||
.map(GetSuggestionsDatabaseResult.tupled)
|
||||
.pipeTo(sender())
|
||||
|
||||
case Completion(_, _, selfType, returnType, tags) =>
|
||||
val kinds = tags.map(_.map(SuggestionKind.toSuggestion))
|
||||
repo
|
||||
.search(selfType, returnType, kinds)
|
||||
.map(CompletionResult.tupled)
|
||||
case Completion(path, pos, selfType, returnType, tags) =>
|
||||
getModule(projectName, path)
|
||||
.fold(
|
||||
Future.successful,
|
||||
module =>
|
||||
repo
|
||||
.search(
|
||||
Some(module),
|
||||
selfType,
|
||||
returnType,
|
||||
tags.map(_.map(SuggestionKind.toSuggestion)),
|
||||
Some(toPosition(pos))
|
||||
)
|
||||
.map(CompletionResult.tupled)
|
||||
)
|
||||
.pipeTo(sender())
|
||||
|
||||
case FileDeletedEvent(path) =>
|
||||
getModule(projectName, path)
|
||||
.fold(
|
||||
err => Future.successful(Left(err)),
|
||||
module =>
|
||||
repo
|
||||
.removeByModule(module)
|
||||
.map {
|
||||
case (version, ids) =>
|
||||
Right(
|
||||
SuggestionsDatabaseUpdateNotification(
|
||||
version,
|
||||
ids.map(SuggestionsDatabaseUpdate.Remove)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.onComplete {
|
||||
case Success(Right(notification)) =>
|
||||
if (notification.updates.nonEmpty) {
|
||||
clients.foreach { clientId =>
|
||||
sessionRouter ! DeliverToJsonController(clientId, notification)
|
||||
}
|
||||
}
|
||||
case Success(Left(err)) =>
|
||||
log.error(
|
||||
"Error cleaning the index after file delete event: {}",
|
||||
err
|
||||
)
|
||||
case Failure(ex) =>
|
||||
log.error(
|
||||
ex,
|
||||
"Error cleaning the index after file delete event"
|
||||
)
|
||||
}
|
||||
|
||||
case ProjectNameChangedEvent(name) =>
|
||||
context.become(initialized(name, clients))
|
||||
}
|
||||
|
||||
private def toGetSuggestionsDatabaseResult(
|
||||
version: Long,
|
||||
entries: Seq[SuggestionEntry]
|
||||
): GetSuggestionsDatabaseResult = {
|
||||
val updates = entries.map(entry =>
|
||||
SuggestionsDatabaseUpdate.Add(entry.id, entry.suggestion)
|
||||
)
|
||||
GetSuggestionsDatabaseResult(updates, version)
|
||||
/**
|
||||
* Handle the suggestions database re-index update.
|
||||
*
|
||||
* Function clears existing module suggestions from the database, inserts new
|
||||
* suggestions and builds the notification containing combined removed and
|
||||
* added suggestions.
|
||||
*
|
||||
* @param moduleName the module name
|
||||
* @param updates the list of updates after the full module re-index
|
||||
* @return the API suggestions database update notification
|
||||
*/
|
||||
private def applyReIndexUpdates(
|
||||
moduleName: String,
|
||||
updates: Seq[Api.SuggestionsDatabaseUpdate.Add]
|
||||
): Future[SuggestionsDatabaseUpdateNotification] = {
|
||||
val added = updates.map(_.suggestion)
|
||||
for {
|
||||
(_, removedIds) <- repo.removeByModule(moduleName)
|
||||
(version, addedIds) <- repo.insertAll(added)
|
||||
} yield {
|
||||
val updatesRemoved = removedIds.map(SuggestionsDatabaseUpdate.Remove)
|
||||
val updatesAdded = (addedIds zip added).flatMap {
|
||||
case (Some(id), suggestion) =>
|
||||
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
|
||||
case (None, suggestion) =>
|
||||
log.error("failed to insert suggestion: {}", suggestion)
|
||||
None
|
||||
}
|
||||
SuggestionsDatabaseUpdateNotification(
|
||||
version,
|
||||
updatesRemoved :++ updatesAdded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the suggestions database update.
|
||||
*
|
||||
* Function applies notification updates on the suggestions database and
|
||||
* builds the notification to the user
|
||||
*
|
||||
* @param msg the suggestions database update notification from runtime
|
||||
* @return the API suggestions database update notification
|
||||
*/
|
||||
private def applyDatabaseUpdates(
|
||||
msg: Api.SuggestionsDatabaseUpdateNotification
|
||||
): Future[SuggestionsDatabaseUpdateNotification] = {
|
||||
val (added, removed) = msg.updates
|
||||
.foldLeft((Seq[Suggestion](), Seq[Suggestion]())) {
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Add) =>
|
||||
(add :+ msg.suggestion, remove)
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Remove) =>
|
||||
(add, remove :+ msg.suggestion)
|
||||
}
|
||||
|
||||
for {
|
||||
(_, removedIds) <- repo.removeAll(removed)
|
||||
(version, addedIds) <- repo.insertAll(added)
|
||||
} yield {
|
||||
val updatesRemoved = removedIds.collect {
|
||||
case Some(id) => SuggestionsDatabaseUpdate.Remove(id)
|
||||
}
|
||||
val updatesAdded =
|
||||
(addedIds zip added).flatMap {
|
||||
case (Some(id), suggestion) =>
|
||||
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
|
||||
case (None, suggestion) =>
|
||||
log.error("failed to insert suggestion: {}", suggestion)
|
||||
None
|
||||
}
|
||||
SuggestionsDatabaseUpdateNotification(
|
||||
version,
|
||||
updatesRemoved :++ updatesAdded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the module name from the requested file path.
|
||||
*
|
||||
* @param projectName the project name
|
||||
* @param path the requested file path
|
||||
* @return the module name
|
||||
*/
|
||||
private def getModule(
|
||||
projectName: String,
|
||||
path: Path
|
||||
): Either[SearchFailure, String] =
|
||||
for {
|
||||
rootFile <- config.findContentRoot(path.rootId).left.map(FileSystemError)
|
||||
module <-
|
||||
ModuleNameBuilder
|
||||
.build(projectName, rootFile.toPath, path.toFile(rootFile).toPath)
|
||||
.toRight(ModuleNameNotResolvedError(path))
|
||||
} yield module
|
||||
|
||||
private def toPosition(pos: Position): Suggestion.Position =
|
||||
Suggestion.Position(pos.line, pos.character)
|
||||
}
|
||||
|
||||
object SuggestionsHandler {
|
||||
@ -57,9 +338,15 @@ object SuggestionsHandler {
|
||||
/**
|
||||
* Creates a configuration object used to create a [[SuggestionsHandler]].
|
||||
*
|
||||
* @param config the server configuration
|
||||
* @param repo the suggestions repo
|
||||
* @param sessionRouter the session router
|
||||
*/
|
||||
def props(repo: SuggestionsRepo[Future]): Props =
|
||||
Props(new SuggestionsHandler(repo))
|
||||
def props(
|
||||
config: Config,
|
||||
repo: SuggestionsRepo[Future],
|
||||
sessionRouter: ActorRef
|
||||
): Props =
|
||||
Props(new SuggestionsHandler(config, repo, sessionRouter))
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ import org.enso.languageserver.text.TextProtocol.{
|
||||
OpenFile,
|
||||
SaveFile
|
||||
}
|
||||
import org.enso.searcher.FileVersionsRepo
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* An actor that routes request regarding text editing to the right buffer.
|
||||
@ -55,12 +58,17 @@ import org.enso.languageserver.text.TextProtocol.{
|
||||
*
|
||||
* }}}
|
||||
*
|
||||
* @param versionsRepo a repo containing versions of indexed files
|
||||
* @param fileManager a file manager
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param versionCalculator a content based version calculator
|
||||
*/
|
||||
class BufferRegistry(fileManager: ActorRef, runtimeConnector: ActorRef)(
|
||||
implicit versionCalculator: ContentBasedVersioning
|
||||
class BufferRegistry(
|
||||
versionsRepo: FileVersionsRepo[Future],
|
||||
fileManager: ActorRef,
|
||||
runtimeConnector: ActorRef
|
||||
)(implicit
|
||||
versionCalculator: ContentBasedVersioning
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
@ -77,7 +85,12 @@ class BufferRegistry(fileManager: ActorRef, runtimeConnector: ActorRef)(
|
||||
} else {
|
||||
val bufferRef =
|
||||
context.actorOf(
|
||||
CollaborativeBuffer.props(path, fileManager, runtimeConnector)
|
||||
CollaborativeBuffer.props(
|
||||
path,
|
||||
versionsRepo,
|
||||
fileManager,
|
||||
runtimeConnector
|
||||
)
|
||||
)
|
||||
context.watch(bufferRef)
|
||||
bufferRef.forward(msg)
|
||||
@ -130,14 +143,19 @@ object BufferRegistry {
|
||||
/**
|
||||
* Creates a configuration object used to create a [[BufferRegistry]]
|
||||
*
|
||||
* @param versionsRepo a repo containing versions of indexed files
|
||||
* @param fileManager a file manager actor
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param versionCalculator a content based version calculator
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(fileManager: ActorRef, runtimeConnector: ActorRef)(
|
||||
implicit versionCalculator: ContentBasedVersioning
|
||||
def props(
|
||||
versionsRepo: FileVersionsRepo[Future],
|
||||
fileManager: ActorRef,
|
||||
runtimeConnector: ActorRef
|
||||
)(implicit
|
||||
versionCalculator: ContentBasedVersioning
|
||||
): Props =
|
||||
Props(new BufferRegistry(fileManager, runtimeConnector))
|
||||
Props(new BufferRegistry(versionsRepo, fileManager, runtimeConnector))
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package org.enso.languageserver.text
|
||||
|
||||
import java.util
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Stash}
|
||||
import akka.pattern.pipe
|
||||
import cats.implicits._
|
||||
import org.enso.languageserver.capability.CapabilityProtocol._
|
||||
import org.enso.languageserver.data.{
|
||||
@ -30,9 +33,11 @@ import org.enso.languageserver.text.CollaborativeBuffer.IOTimeout
|
||||
import org.enso.languageserver.text.TextProtocol._
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.FileVersionsRepo
|
||||
import org.enso.text.editing._
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.language.postfixOps
|
||||
|
||||
@ -40,6 +45,7 @@ import scala.language.postfixOps
|
||||
* An actor enabling multiple users edit collaboratively a file.
|
||||
*
|
||||
* @param bufferPath a path to a file
|
||||
* @param versionsRepo a repo containing versions of indexed files
|
||||
* @param fileManager a file manger actor
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param timeout a request timeout
|
||||
@ -47,11 +53,12 @@ import scala.language.postfixOps
|
||||
*/
|
||||
class CollaborativeBuffer(
|
||||
bufferPath: Path,
|
||||
versionsRepo: FileVersionsRepo[Future],
|
||||
fileManager: ActorRef,
|
||||
runtimeConnector: ActorRef,
|
||||
timeout: FiniteDuration
|
||||
)(
|
||||
implicit versionCalculator: ContentBasedVersioning
|
||||
)(implicit
|
||||
versionCalculator: ContentBasedVersioning
|
||||
) extends Actor
|
||||
with Stash
|
||||
with ActorLogging
|
||||
@ -284,9 +291,17 @@ class CollaborativeBuffer(
|
||||
originalSender ! OpenFileResponse(
|
||||
Right(OpenFileResult(buffer, Some(cap)))
|
||||
)
|
||||
runtimeConnector ! Api.Request(
|
||||
Api.OpenFileNotification(file.path, file.content)
|
||||
)
|
||||
val currentVersion = versionCalculator.evalDigest(file.content)
|
||||
versionsRepo
|
||||
.setVersion(file.path, currentVersion)
|
||||
.map { prevVersionOpt =>
|
||||
val isIndexed =
|
||||
prevVersionOpt.exists(util.Arrays.equals(_, currentVersion))
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(file.path, file.content, isIndexed)
|
||||
)
|
||||
}
|
||||
.pipeTo(runtimeConnector)
|
||||
context.become(
|
||||
collaborativeEditing(
|
||||
buffer,
|
||||
@ -325,8 +340,8 @@ class CollaborativeBuffer(
|
||||
): Unit = {
|
||||
val newLock =
|
||||
lockHolder.flatMap {
|
||||
case holder if (holder.clientId == clientId) => None
|
||||
case holder => Some(holder)
|
||||
case holder if holder.clientId == clientId => None
|
||||
case holder => Some(holder)
|
||||
}
|
||||
val newClientMap = clients - clientId
|
||||
if (newClientMap.isEmpty) {
|
||||
@ -398,6 +413,7 @@ object CollaborativeBuffer {
|
||||
* Creates a configuration object used to create a [[CollaborativeBuffer]]
|
||||
*
|
||||
* @param bufferPath a path to a file
|
||||
* @param versionsRepo a repo containing versions of indexed files
|
||||
* @param fileManager a file manager actor
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param timeout a request timeout
|
||||
@ -406,6 +422,7 @@ object CollaborativeBuffer {
|
||||
*/
|
||||
def props(
|
||||
bufferPath: Path,
|
||||
versionsRepo: FileVersionsRepo[Future],
|
||||
fileManager: ActorRef,
|
||||
runtimeConnector: ActorRef,
|
||||
timeout: FiniteDuration = 10 seconds
|
||||
@ -413,6 +430,7 @@ object CollaborativeBuffer {
|
||||
Props(
|
||||
new CollaborativeBuffer(
|
||||
bufferPath,
|
||||
versionsRepo,
|
||||
fileManager,
|
||||
runtimeConnector,
|
||||
timeout
|
||||
|
@ -8,6 +8,10 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="ERROR"/>
|
||||
<logger name="slick" level="INFO"/>
|
||||
<logger name="slick.compiler" level="INFO"/>
|
||||
|
||||
<root level="ERROR">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
@ -3,6 +3,7 @@ package org.enso.languageserver.filemanager
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.languageserver.effect.Effects
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@ -70,7 +71,7 @@ class FileSystemSpec extends AnyFlatSpec with Matchers with Effects {
|
||||
//given
|
||||
val path = Paths.get(testDirPath.toString, "foo.txt")
|
||||
val content = "123456789"
|
||||
testDir.delete()
|
||||
testDirPath.toFile.delete()
|
||||
//when
|
||||
val result =
|
||||
objectUnderTest.write(path.toFile, content).unsafeRunSync()
|
||||
@ -654,9 +655,7 @@ class FileSystemSpec extends AnyFlatSpec with Matchers with Effects {
|
||||
trait TestCtx {
|
||||
|
||||
val testDirPath = Files.createTempDirectory(null)
|
||||
|
||||
val testDir = testDirPath.toFile
|
||||
testDir.deleteOnExit()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testDirPath.toFile))
|
||||
|
||||
val objectUnderTest = new FileSystem
|
||||
|
||||
|
@ -22,7 +22,7 @@ class WatcherAdapterSpec
|
||||
|
||||
final val Timeout: FiniteDuration = 5.seconds
|
||||
|
||||
it should "get create events" taggedAs Retry() in withWatcher {
|
||||
it should "get create events" taggedAs Retry in withWatcher {
|
||||
(path, events) =>
|
||||
val fileA = Paths.get(path.toString, "a.txt")
|
||||
Files.createFile(fileA)
|
||||
@ -30,7 +30,7 @@ class WatcherAdapterSpec
|
||||
event shouldBe WatcherAdapter.WatcherEvent(fileA, EventTypeCreate)
|
||||
}
|
||||
|
||||
it should "get delete events" taggedAs Retry() in withWatcher {
|
||||
it should "get delete events" taggedAs Retry in withWatcher {
|
||||
(path, events) =>
|
||||
val fileA = Paths.get(path.toString, "a.txt")
|
||||
|
||||
@ -43,7 +43,7 @@ class WatcherAdapterSpec
|
||||
event2 shouldBe WatcherEvent(fileA, EventTypeDelete)
|
||||
}
|
||||
|
||||
it should "get modify events" taggedAs Retry() in withWatcher {
|
||||
it should "get modify events" taggedAs Retry in withWatcher {
|
||||
(path, events) =>
|
||||
val fileA = Paths.get(path.toString, "a.txt")
|
||||
|
||||
@ -56,7 +56,7 @@ class WatcherAdapterSpec
|
||||
event2 shouldBe WatcherEvent(fileA, EventTypeModify)
|
||||
}
|
||||
|
||||
it should "get events from subdirectories" taggedAs Retry() in withWatcher {
|
||||
it should "get events from subdirectories" taggedAs Retry in withWatcher {
|
||||
(path, events) =>
|
||||
val subdir = Paths.get(path.toString, "subdir")
|
||||
val fileA = Paths.get(path.toString, "subdir", "a.txt")
|
||||
|
@ -1,12 +1,16 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import org.enso.searcher.Suggestion
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.polyglot.Suggestion
|
||||
|
||||
/** Suggestion instances used in tests. */
|
||||
object Suggestions {
|
||||
|
||||
val atom: Suggestion.Atom =
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test.Main",
|
||||
name = "MyType",
|
||||
arguments = Vector(Suggestion.Argument("a", "Any", false, false, None)),
|
||||
returnType = "MyAtom",
|
||||
@ -15,7 +19,10 @@ object Suggestions {
|
||||
|
||||
val method: Suggestion.Method =
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId =
|
||||
Some(UUID.fromString("ea9d7734-26a7-4f65-9dd9-c648eaf57d63")),
|
||||
module = "Test.Main",
|
||||
name = "foo",
|
||||
arguments = Vector(
|
||||
Suggestion.Argument("this", "MyType", false, false, None),
|
||||
Suggestion.Argument("foo", "Number", false, true, Some("42"))
|
||||
@ -27,17 +34,25 @@ object Suggestions {
|
||||
|
||||
val function: Suggestion.Function =
|
||||
Suggestion.Function(
|
||||
externalId =
|
||||
Some(UUID.fromString("78d452ce-ed48-48f1-b4f2-b7f45f8dff89")),
|
||||
module = "Test.Main",
|
||||
name = "print",
|
||||
arguments = Vector(),
|
||||
returnType = "IO",
|
||||
scope = Suggestion.Scope(9, 22)
|
||||
scope =
|
||||
Suggestion.Scope(Suggestion.Position(1, 9), Suggestion.Position(1, 22))
|
||||
)
|
||||
|
||||
val local: Suggestion.Local =
|
||||
Suggestion.Local(
|
||||
externalId =
|
||||
Some(UUID.fromString("dc077227-d9b6-4620-9b51-792c2a69419d")),
|
||||
module = "Test.Main",
|
||||
name = "x",
|
||||
returnType = "Number",
|
||||
scope = Suggestion.Scope(34, 68)
|
||||
scope =
|
||||
Suggestion.Scope(Suggestion.Position(21, 0), Suggestion.Position(89, 0))
|
||||
)
|
||||
|
||||
val all = Seq(atom, method, function, local)
|
||||
|
@ -1,148 +0,0 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
|
||||
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||
AcquireCapability,
|
||||
CapabilityAcquired
|
||||
}
|
||||
import org.enso.languageserver.data.{
|
||||
CapabilityRegistration,
|
||||
ReceivesSuggestionsDatabaseUpdates
|
||||
}
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.SuggestionsRepo
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
class SuggestionsDatabaseEventsListenerTest
|
||||
extends TestKit(ActorSystem("TestSystem"))
|
||||
with ImplicitSender
|
||||
with AnyWordSpecLike
|
||||
with Matchers
|
||||
with BeforeAndAfterAll
|
||||
with RetrySpec {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val Timeout: FiniteDuration = 5.seconds
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
"SuggestionsHandler" should {
|
||||
|
||||
"subscribe to notification updates" taggedAs Retry() in withDb {
|
||||
(router, repo) =>
|
||||
val handler = newEventsListener(router.ref, repo)
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
}
|
||||
|
||||
"receive runtime updates" taggedAs Retry() in withDb { (router, repo) =>
|
||||
val handler = newEventsListener(router.ref, repo)
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
// acquire capability
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
|
||||
// receive updates
|
||||
handler ! Api.SuggestionsDatabaseUpdateNotification(
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add)
|
||||
)
|
||||
|
||||
val updates = Suggestions.all.zipWithIndex.map {
|
||||
case (suggestion, ix) =>
|
||||
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
|
||||
}
|
||||
router.expectMsg(
|
||||
DeliverToJsonController(
|
||||
clientId,
|
||||
SearchProtocol.SuggestionsDatabaseUpdateNotification(updates, 4L)
|
||||
)
|
||||
)
|
||||
|
||||
// check database entries exist
|
||||
val (_, records) = Await.result(repo.getAll, Timeout)
|
||||
records.map(_.suggestion) should contain theSameElementsAs Suggestions.all
|
||||
}
|
||||
|
||||
"apply runtime updates in correct order" taggedAs Retry() in withDb {
|
||||
(router, repo) =>
|
||||
val handler = newEventsListener(router.ref, repo)
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
// acquire capability
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
|
||||
// receive updates
|
||||
handler ! Api.SuggestionsDatabaseUpdateNotification(
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add) ++
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Remove)
|
||||
)
|
||||
|
||||
val updates = Suggestions.all.zipWithIndex.map {
|
||||
case (suggestion, ix) =>
|
||||
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
|
||||
}
|
||||
router.expectMsg(
|
||||
DeliverToJsonController(
|
||||
clientId,
|
||||
SearchProtocol.SuggestionsDatabaseUpdateNotification(updates, 4L)
|
||||
)
|
||||
)
|
||||
|
||||
// check that database entries removed
|
||||
val (_, all) = Await.result(repo.getAll, Timeout)
|
||||
all.map(_.suggestion) should contain theSameElementsAs Suggestions.all
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def newEventsListener(
|
||||
router: ActorRef,
|
||||
repo: SuggestionsRepo[Future]
|
||||
): ActorRef =
|
||||
system.actorOf(SuggestionsDatabaseEventsListener.props(router, repo))
|
||||
|
||||
def newJsonSession(clientId: UUID): JsonSession =
|
||||
JsonSession(clientId, TestProbe().ref)
|
||||
|
||||
def withDb(
|
||||
test: (TestProbe, SuggestionsRepo[Future]) => Any
|
||||
): Unit = {
|
||||
val dbPath = Files.createTempFile("suggestions", ".db")
|
||||
system.registerOnTermination(Files.deleteIfExists(dbPath))
|
||||
val repo = SqlSuggestionsRepo()
|
||||
Await.ready(repo.init, Timeout)
|
||||
val router = TestProbe("sessionRouterProbe")
|
||||
|
||||
try test(router, repo)
|
||||
finally repo.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
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
|
||||
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||
AcquireCapability,
|
||||
CapabilityAcquired
|
||||
}
|
||||
import org.enso.languageserver.data.{
|
||||
CapabilityRegistration,
|
||||
Config,
|
||||
DirectoriesConfig,
|
||||
ExecutionContextConfig,
|
||||
FileManagerConfig,
|
||||
PathWatcherConfig,
|
||||
ReceivesSuggestionsDatabaseUpdates
|
||||
}
|
||||
import org.enso.languageserver.filemanager.Path
|
||||
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.{SuggestionEntry, SuggestionsRepo}
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
import org.enso.text.editing.model.Position
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
class SuggestionsHandlerSpec
|
||||
extends TestKit(ActorSystem("TestSystem"))
|
||||
with ImplicitSender
|
||||
with AnyWordSpecLike
|
||||
with Matchers
|
||||
with BeforeAndAfterAll
|
||||
with RetrySpec {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val Timeout: FiniteDuration = 10.seconds
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
"SuggestionsHandler" should {
|
||||
|
||||
"subscribe to notification updates" taggedAs Retry in withDb {
|
||||
(_, _, _, handler) =>
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
}
|
||||
|
||||
"receive runtime updates" taggedAs Retry in withDb {
|
||||
(_, repo, router, handler) =>
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
// acquire capability
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
|
||||
// receive updates
|
||||
handler ! Api.SuggestionsDatabaseUpdateNotification(
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add)
|
||||
)
|
||||
|
||||
val updates = Suggestions.all.zipWithIndex.map {
|
||||
case (suggestion, ix) =>
|
||||
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
|
||||
}
|
||||
router.expectMsg(
|
||||
DeliverToJsonController(
|
||||
clientId,
|
||||
SearchProtocol.SuggestionsDatabaseUpdateNotification(4L, updates)
|
||||
)
|
||||
)
|
||||
|
||||
// check database entries exist
|
||||
val (_, records) = Await.result(repo.getAll, Timeout)
|
||||
records.map(
|
||||
_.suggestion
|
||||
) should contain theSameElementsAs Suggestions.all
|
||||
}
|
||||
|
||||
"apply runtime updates in correct order" taggedAs Retry in withDb {
|
||||
(_, repo, router, handler) =>
|
||||
val clientId = UUID.randomUUID()
|
||||
|
||||
// acquire capability
|
||||
handler ! AcquireCapability(
|
||||
newJsonSession(clientId),
|
||||
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
|
||||
)
|
||||
expectMsg(CapabilityAcquired)
|
||||
|
||||
// receive updates
|
||||
handler ! Api.SuggestionsDatabaseUpdateNotification(
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add) ++
|
||||
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Remove)
|
||||
)
|
||||
|
||||
val updates = Suggestions.all.zipWithIndex.map {
|
||||
case (suggestion, ix) =>
|
||||
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
|
||||
}
|
||||
router.expectMsg(
|
||||
DeliverToJsonController(
|
||||
clientId,
|
||||
SearchProtocol.SuggestionsDatabaseUpdateNotification(4L, updates)
|
||||
)
|
||||
)
|
||||
|
||||
// check that database entries removed
|
||||
val (_, all) = Await.result(repo.getAll, Timeout)
|
||||
all.map(_.suggestion) should contain theSameElementsAs Suggestions.all
|
||||
}
|
||||
|
||||
"get initial suggestions database version" taggedAs Retry in withDb {
|
||||
(_, _, _, handler) =>
|
||||
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(0))
|
||||
}
|
||||
|
||||
"get suggestions database version" taggedAs Retry in withDb {
|
||||
(_, repo, _, handler) =>
|
||||
Await.ready(repo.insert(Suggestions.atom), Timeout)
|
||||
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(1))
|
||||
}
|
||||
|
||||
"get initial suggestions database" taggedAs Retry in withDb {
|
||||
(_, _, _, handler) =>
|
||||
handler ! SearchProtocol.GetSuggestionsDatabase
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseResult(0, Seq()))
|
||||
}
|
||||
|
||||
"get suggestions database" taggedAs Retry in withDb {
|
||||
(_, repo, _, handler) =>
|
||||
Await.ready(repo.insert(Suggestions.atom), Timeout)
|
||||
handler ! SearchProtocol.GetSuggestionsDatabase
|
||||
|
||||
expectMsg(
|
||||
SearchProtocol.GetSuggestionsDatabaseResult(
|
||||
1,
|
||||
Seq(SuggestionEntry(1L, Suggestions.atom))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"search entries by empty search query" taggedAs Retry in withDb {
|
||||
(config, repo, _, handler) =>
|
||||
Await.ready(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
file = mkModulePath(config, "Foo", "Main.enso"),
|
||||
position = Position(0, 0),
|
||||
selfType = None,
|
||||
returnType = None,
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq()))
|
||||
}
|
||||
|
||||
"search entries by self type" taggedAs Retry in withDb {
|
||||
(config, repo, _, handler) =>
|
||||
val (_, Seq(_, methodId, _, _)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
file = mkModulePath(config, "Main.enso"),
|
||||
position = Position(0, 0),
|
||||
selfType = Some("MyType"),
|
||||
returnType = None,
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(methodId).flatten))
|
||||
}
|
||||
|
||||
"search entries by return type" taggedAs Retry in withDb {
|
||||
(config, repo, _, handler) =>
|
||||
val (_, Seq(_, _, functionId, _)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
file = mkModulePath(config, "Main.enso"),
|
||||
position = Position(1, 10),
|
||||
selfType = None,
|
||||
returnType = Some("IO"),
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(functionId).flatten))
|
||||
}
|
||||
|
||||
"search entries by tags" taggedAs Retry in withDb {
|
||||
(config, repo, _, handler) =>
|
||||
val (_, Seq(_, _, _, localId)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
file = mkModulePath(config, "Main.enso"),
|
||||
position = Position(42, 0),
|
||||
selfType = None,
|
||||
returnType = None,
|
||||
tags = Some(Seq(SearchProtocol.SuggestionKind.Local))
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(localId).flatten))
|
||||
}
|
||||
}
|
||||
|
||||
def newSuggestionsHandler(
|
||||
config: Config,
|
||||
sessionRouter: TestProbe,
|
||||
repo: SuggestionsRepo[Future]
|
||||
): ActorRef = {
|
||||
val handler =
|
||||
system.actorOf(SuggestionsHandler.props(config, repo, sessionRouter.ref))
|
||||
handler ! ProjectNameChangedEvent("Test")
|
||||
handler
|
||||
}
|
||||
|
||||
def newConfig(root: File): Config = {
|
||||
Config(
|
||||
Map(UUID.randomUUID() -> root),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds),
|
||||
DirectoriesConfig(root)
|
||||
)
|
||||
}
|
||||
|
||||
def mkModulePath(config: Config, segments: String*): Path = {
|
||||
val (rootId, _) = config.contentRoots.head
|
||||
Path(rootId, "src" +: segments.toVector)
|
||||
}
|
||||
|
||||
def newJsonSession(clientId: UUID): JsonSession =
|
||||
JsonSession(clientId, TestProbe().ref)
|
||||
|
||||
def withDb(
|
||||
test: (Config, SuggestionsRepo[Future], TestProbe, ActorRef) => Any
|
||||
): Unit = {
|
||||
val testContentRoot = Files.createTempDirectory(null).toRealPath()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
|
||||
val config = newConfig(testContentRoot.toFile)
|
||||
val router = TestProbe("session-router")
|
||||
val repo = SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile)
|
||||
val handler = newSuggestionsHandler(config, router, repo)
|
||||
Await.ready(repo.init, Timeout)
|
||||
|
||||
try test(config, repo, router, handler)
|
||||
finally {
|
||||
system.stop(handler)
|
||||
repo.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
package org.enso.languageserver.runtime
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.testkit.{ImplicitSender, TestKit}
|
||||
import org.enso.searcher.SuggestionsRepo
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
import org.enso.text.editing.model.Position
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
import org.enso.testkit.RetrySpec
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
class SuggestionsHandlerTest
|
||||
extends TestKit(ActorSystem("TestSystem"))
|
||||
with ImplicitSender
|
||||
with AnyWordSpecLike
|
||||
with BeforeAndAfterAll
|
||||
with RetrySpec {
|
||||
|
||||
import system.dispatcher
|
||||
|
||||
val Timeout: FiniteDuration = 10.seconds
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
"SuggestionsHandler" should {
|
||||
|
||||
"get initial suggestions database version" in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(0))
|
||||
}
|
||||
|
||||
"get suggestions database version" in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
Await.ready(repo.insert(Suggestions.atom), Timeout)
|
||||
|
||||
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(1))
|
||||
}
|
||||
|
||||
"get initial suggestions database" in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
handler ! SearchProtocol.GetSuggestionsDatabase
|
||||
|
||||
expectMsg(SearchProtocol.GetSuggestionsDatabaseResult(Seq(), 0))
|
||||
}
|
||||
|
||||
"get suggestions database" in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
Await.ready(repo.insert(Suggestions.atom), Timeout)
|
||||
handler ! SearchProtocol.GetSuggestionsDatabase
|
||||
|
||||
expectMsg(
|
||||
SearchProtocol.GetSuggestionsDatabaseResult(
|
||||
Seq(
|
||||
SearchProtocol.SuggestionsDatabaseUpdate.Add(1L, Suggestions.atom)
|
||||
),
|
||||
1
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"search entries by empty search query" taggedAs Retry() in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
Await.ready(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
module = "Test.Main",
|
||||
position = Position(0, 0),
|
||||
selfType = None,
|
||||
returnType = None,
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq()))
|
||||
}
|
||||
|
||||
"search entries by self type" taggedAs Retry() in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
val (_, Seq(_, methodId, _, _)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
module = "Test.Main",
|
||||
position = Position(0, 0),
|
||||
selfType = Some("MyType"),
|
||||
returnType = None,
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(methodId).flatten))
|
||||
}
|
||||
|
||||
"search entries by return type" taggedAs Retry() in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
val (_, Seq(_, _, functionId, _)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
module = "Test.Main",
|
||||
position = Position(0, 0),
|
||||
selfType = None,
|
||||
returnType = Some("IO"),
|
||||
tags = None
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(functionId).flatten))
|
||||
}
|
||||
|
||||
"search entries by tags" taggedAs Retry() in withDb { repo =>
|
||||
val handler = newSuggestionsHandler(repo)
|
||||
val (_, Seq(_, _, _, localId)) =
|
||||
Await.result(repo.insertAll(Suggestions.all), Timeout)
|
||||
handler ! SearchProtocol.Completion(
|
||||
module = "Test.Main",
|
||||
position = Position(0, 0),
|
||||
selfType = None,
|
||||
returnType = None,
|
||||
tags = Some(Seq(SearchProtocol.SuggestionKind.Local))
|
||||
)
|
||||
|
||||
expectMsg(SearchProtocol.CompletionResult(4L, Seq(localId).flatten))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def newSuggestionsHandler(repo: SuggestionsRepo[Future]): ActorRef =
|
||||
system.actorOf(SuggestionsHandler.props(repo))
|
||||
|
||||
def withDb(test: SuggestionsRepo[Future] => Any): Unit = {
|
||||
val dbPath = Files.createTempFile("suggestions", ".db")
|
||||
system.registerOnTermination(Files.deleteIfExists(dbPath))
|
||||
val repo = SqlSuggestionsRepo()
|
||||
Await.ready(repo.init, Timeout)
|
||||
|
||||
try test(repo)
|
||||
finally repo.close()
|
||||
}
|
||||
|
||||
}
|
@ -6,8 +6,10 @@ import java.util.UUID
|
||||
import akka.actor.{ActorRef, Props}
|
||||
import akka.http.scaladsl.model.RemoteAddress
|
||||
import com.google.flatbuffers.FlatBufferBuilder
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.languageserver.data.{
|
||||
Config,
|
||||
DirectoriesConfig,
|
||||
ExecutionContextConfig,
|
||||
FileManagerConfig,
|
||||
PathWatcherConfig
|
||||
@ -32,10 +34,11 @@ class BaseBinaryServerTest extends BinaryServerTestKit {
|
||||
Map(testContentRootId -> testContentRoot.toFile),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds)
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds),
|
||||
DirectoriesConfig(testContentRoot.toFile)
|
||||
)
|
||||
|
||||
testContentRoot.toFile.deleteOnExit()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
|
||||
|
||||
@volatile
|
||||
protected var lastConnectionController: ActorRef = _
|
||||
|
@ -5,6 +5,7 @@ import java.util.UUID
|
||||
|
||||
import akka.testkit.TestProbe
|
||||
import io.circe.literal._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.jsonrpc.test.JsonRpcServerTestKit
|
||||
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
|
||||
import org.enso.languageserver.capability.CapabilityRouter
|
||||
@ -20,14 +21,10 @@ import org.enso.languageserver.protocol.json.{
|
||||
JsonConnectionControllerFactory,
|
||||
JsonRpc
|
||||
}
|
||||
import org.enso.languageserver.runtime.{
|
||||
ContextRegistry,
|
||||
SuggestionsDatabaseEventsListener,
|
||||
SuggestionsHandler
|
||||
}
|
||||
import org.enso.languageserver.runtime.{ContextRegistry, SuggestionsHandler}
|
||||
import org.enso.languageserver.session.SessionRouter
|
||||
import org.enso.languageserver.text.BufferRegistry
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
import org.enso.searcher.sql.{SqlSuggestionsRepo, SqlVersionsRepo}
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
@ -36,19 +33,21 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
|
||||
val timeout: FiniteDuration = 3.seconds
|
||||
|
||||
val testSuggestionsDbPath = Files.createTempFile("suggestions", ".db")
|
||||
val testContentRoot = Files.createTempDirectory(null).toRealPath()
|
||||
val testContentRootId = UUID.randomUUID()
|
||||
val config = Config(
|
||||
Map(testContentRootId -> testContentRoot.toFile),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds)
|
||||
)
|
||||
val config = mkConfig
|
||||
val runtimeConnectorProbe = TestProbe()
|
||||
|
||||
testContentRoot.toFile.deleteOnExit()
|
||||
testSuggestionsDbPath.toFile.deleteOnExit()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile))
|
||||
|
||||
def mkConfig: Config =
|
||||
Config(
|
||||
Map(testContentRootId -> testContentRoot.toFile),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds),
|
||||
DirectoriesConfig(testContentRoot.toFile)
|
||||
)
|
||||
|
||||
override def protocol: Protocol = JsonRpc.protocol
|
||||
|
||||
@ -78,15 +77,27 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
)
|
||||
|
||||
override def clientControllerFactory: ClientControllerFactory = {
|
||||
val zioExec = ZioExec(zio.Runtime.default)
|
||||
val suggestionsRepo = SqlSuggestionsRepo()(system.dispatcher)
|
||||
val zioExec = ZioExec(zio.Runtime.default)
|
||||
val suggestionsRepo =
|
||||
SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile)(
|
||||
system.dispatcher
|
||||
)
|
||||
val versionsRepo =
|
||||
SqlVersionsRepo(config.directories.suggestionsDatabaseFile)(
|
||||
system.dispatcher
|
||||
)
|
||||
Await.ready(suggestionsRepo.init, timeout)
|
||||
Await.ready(versionsRepo.init, timeout)
|
||||
|
||||
val fileManager =
|
||||
system.actorOf(FileManager.props(config, new FileSystem, zioExec))
|
||||
val bufferRegistry =
|
||||
system.actorOf(
|
||||
BufferRegistry.props(fileManager, runtimeConnectorProbe.ref)(
|
||||
BufferRegistry.props(
|
||||
versionsRepo,
|
||||
fileManager,
|
||||
runtimeConnectorProbe.ref
|
||||
)(
|
||||
Sha3_224VersionCalculator
|
||||
)
|
||||
)
|
||||
@ -100,9 +111,9 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
ContextRegistry.props(config, runtimeConnectorProbe.ref, sessionRouter)
|
||||
)
|
||||
|
||||
val suggestionsDatabaseEventsListener =
|
||||
val suggestionsHandler =
|
||||
system.actorOf(
|
||||
SuggestionsDatabaseEventsListener.props(sessionRouter, suggestionsRepo)
|
||||
SuggestionsHandler.props(config, suggestionsRepo, sessionRouter)
|
||||
)
|
||||
|
||||
val capabilityRouter =
|
||||
@ -110,13 +121,10 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
CapabilityRouter.props(
|
||||
bufferRegistry,
|
||||
fileEventRegistry,
|
||||
suggestionsDatabaseEventsListener
|
||||
suggestionsHandler
|
||||
)
|
||||
)
|
||||
|
||||
val suggestionsHandler =
|
||||
system.actorOf(SuggestionsHandler.props(suggestionsRepo))
|
||||
|
||||
new JsonConnectionControllerFactory(
|
||||
bufferRegistry,
|
||||
capabilityRouter,
|
||||
|
@ -6,12 +6,28 @@ import java.util.UUID
|
||||
|
||||
import io.circe.literal._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.languageserver.data._
|
||||
import org.enso.testkit.RetrySpec
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class FileManagerTest extends BaseServerTest with RetrySpec {
|
||||
|
||||
override def mkConfig: Config = {
|
||||
val directoriesDir = Files.createTempDirectory(null).toRealPath()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(directoriesDir.toFile))
|
||||
Config(
|
||||
Map(testContentRootId -> testContentRoot.toFile),
|
||||
FileManagerConfig(timeout = 3.seconds),
|
||||
PathWatcherConfig(),
|
||||
ExecutionContextConfig(requestTimeout = 3.seconds),
|
||||
DirectoriesConfig(directoriesDir.toFile)
|
||||
)
|
||||
}
|
||||
|
||||
"File Server" must {
|
||||
|
||||
"write textual content to a file" taggedAs Retry() in {
|
||||
"write textual content to a file" taggedAs Retry in {
|
||||
val client = getInitialisedWsClient()
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
@ -760,6 +776,7 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
|
||||
|
||||
"get a root tree" in withCleanRoot {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
// create:
|
||||
//
|
||||
// base
|
||||
@ -1407,7 +1424,8 @@ class FileManagerTest extends BaseServerTest with RetrySpec {
|
||||
|
||||
// create symlink base3/link -> $testOtherRoot
|
||||
val testOtherRoot = Files.createTempDirectory(null)
|
||||
val symlink = Paths.get(testContentRoot.toString, "base3", "link")
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testOtherRoot.toFile))
|
||||
val symlink = Paths.get(testContentRoot.toString, "base3", "link")
|
||||
Files.createSymbolicLink(symlink, testOtherRoot)
|
||||
Files.isSymbolicLink(symlink) shouldBe true
|
||||
|
||||
|
@ -4,16 +4,16 @@ import java.io.File
|
||||
|
||||
import io.circe.literal._
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.enso.text.editing.model.{Position, Range, TextEdit}
|
||||
|
||||
class FileNotificationsTest extends BaseServerTest with RetrySpec {
|
||||
class FileNotificationsTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
def file(name: String): File = new File(testContentRoot.toFile, name)
|
||||
|
||||
"text operations" should {
|
||||
|
||||
"notify runtime about operations with files" taggedAs Retry() in {
|
||||
"notify runtime about operations with files" taggedAs Flaky in {
|
||||
// Interaction:
|
||||
// 1. Client 1 creates a file.
|
||||
// 2. Client 1 opens the file.
|
||||
@ -87,7 +87,9 @@ class FileNotificationsTest extends BaseServerTest with RetrySpec {
|
||||
""")
|
||||
// 4
|
||||
runtimeConnectorProbe.expectMsg(
|
||||
Api.Request(Api.OpenFileNotification(file("foo.txt"), "123456789"))
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(file("foo.txt"), "123456789", false)
|
||||
)
|
||||
)
|
||||
|
||||
// 5
|
||||
|
@ -1,12 +1,13 @@
|
||||
package org.enso.languageserver.websocket.json
|
||||
|
||||
import io.circe.literal._
|
||||
import org.enso.testkit.FlakySpec
|
||||
|
||||
class MonitoringTest extends BaseServerTest {
|
||||
class MonitoringTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
"Monitoring subsystem" must {
|
||||
|
||||
"reply to ping requests" in {
|
||||
"reply to ping requests" taggedAs Flaky in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
|
@ -46,21 +46,6 @@ object SearchJsonMessages {
|
||||
}
|
||||
"""
|
||||
|
||||
def completion(reqId: Long) =
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "search/completion",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"module": "Test.Main",
|
||||
"position": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def ok(reqId: Long) =
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
|
@ -1,22 +1,19 @@
|
||||
package org.enso.languageserver.websocket.json
|
||||
|
||||
import io.circe.literal._
|
||||
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
|
||||
import org.enso.languageserver.runtime.Suggestions
|
||||
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.scalatest.BeforeAndAfter
|
||||
|
||||
class SuggestionsDatabaseEventsListenerTest
|
||||
extends BaseServerTest
|
||||
with BeforeAndAfter
|
||||
with FlakySpec {
|
||||
class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
lazy val client = getInitialisedWsClient()
|
||||
|
||||
"SuggestionsDatabaseEventListener" must {
|
||||
"SuggestionsHandlerEvents" must {
|
||||
|
||||
"send suggestions database notifications" taggedAs Flaky in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test"))
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
@ -37,6 +34,7 @@ class SuggestionsDatabaseEventsListenerTest
|
||||
"id" : 1,
|
||||
"suggestion" : {
|
||||
"type" : "atom",
|
||||
"module" : "Test.Main",
|
||||
"name" : "MyType",
|
||||
"arguments" : [
|
||||
{
|
||||
@ -72,6 +70,8 @@ class SuggestionsDatabaseEventsListenerTest
|
||||
"id" : 2,
|
||||
"suggestion" : {
|
||||
"type" : "method",
|
||||
"externalId" : "ea9d7734-26a7-4f65-9dd9-c648eaf57d63",
|
||||
"module" : "Test.Main",
|
||||
"name" : "foo",
|
||||
"arguments" : [
|
||||
{
|
||||
@ -116,13 +116,21 @@ class SuggestionsDatabaseEventsListenerTest
|
||||
"id" : 3,
|
||||
"suggestion" : {
|
||||
"type" : "function",
|
||||
"externalId" : "78d452ce-ed48-48f1-b4f2-b7f45f8dff89",
|
||||
"module" : "Test.Main",
|
||||
"name" : "print",
|
||||
"arguments" : [
|
||||
],
|
||||
"returnType" : "IO",
|
||||
"scope" : {
|
||||
"start" : 9,
|
||||
"end" : 22
|
||||
"start" : {
|
||||
"line" : 1,
|
||||
"character" : 9
|
||||
},
|
||||
"end" : {
|
||||
"line" : 1,
|
||||
"character" : 22
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,11 +156,19 @@ class SuggestionsDatabaseEventsListenerTest
|
||||
"id" : 4,
|
||||
"suggestion" : {
|
||||
"type" : "local",
|
||||
"externalId" : "dc077227-d9b6-4620-9b51-792c2a69419d",
|
||||
"module" : "Test.Main",
|
||||
"name" : "x",
|
||||
"returnType" : "Number",
|
||||
"scope" : {
|
||||
"start" : 34,
|
||||
"end" : 68
|
||||
"start" : {
|
||||
"line" : 21,
|
||||
"character" : 0
|
||||
},
|
||||
"end" : {
|
||||
"line" : 89,
|
||||
"character" : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +1,48 @@
|
||||
package org.enso.languageserver.websocket.json
|
||||
import io.circe.literal._
|
||||
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
|
||||
import java.util.UUID
|
||||
|
||||
class SuggestionsHandlerTest extends BaseServerTest {
|
||||
import io.circe.literal._
|
||||
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
|
||||
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
|
||||
import org.enso.testkit.FlakySpec
|
||||
|
||||
class SuggestionsHandlerTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
"SuggestionsHandler" must {
|
||||
|
||||
"get initial suggestions database version" in {
|
||||
"reply with error when uninitialized" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.getSuggestionsDatabaseVersion(0))
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"id" : 0,
|
||||
"result" : {
|
||||
"version" : 0
|
||||
"error" : {
|
||||
"code" : 7002,
|
||||
"message" : "Project not found in the root directory"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"get initial suggestions database" in {
|
||||
"get initial suggestions database version" in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test"))
|
||||
|
||||
client.send(json.getSuggestionsDatabaseVersion(0))
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"id" : 0,
|
||||
"result" : {
|
||||
"currentVersion" : 0
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"get initial suggestions database" taggedAs Flaky in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test"))
|
||||
|
||||
client.send(json.getSuggestionsDatabase(0))
|
||||
client.expectJson(json"""
|
||||
@ -36,10 +57,53 @@ class SuggestionsHandlerTest extends BaseServerTest {
|
||||
""")
|
||||
}
|
||||
|
||||
"reply to completion request" in {
|
||||
"reply to completion request" taggedAs Flaky in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test"))
|
||||
|
||||
client.send(json.completion(0))
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "search/completion",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"file": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "src", "Main.enso" ]
|
||||
},
|
||||
"position": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"id" : 0,
|
||||
"result" : {
|
||||
"results" : [
|
||||
],
|
||||
"currentVersion" : 0
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "search/completion",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"file": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "src", "Foo", "Main.enso" ]
|
||||
},
|
||||
"position": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"id" : 0,
|
||||
@ -51,6 +115,37 @@ class SuggestionsHandlerTest extends BaseServerTest {
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"reply with error when project root not found" taggedAs Flaky in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test"))
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "search/completion",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"file": {
|
||||
"rootId": ${UUID.randomUUID()},
|
||||
"segments": [ "src", "Main.enso" ]
|
||||
},
|
||||
"position": {
|
||||
"line": 0,
|
||||
"character": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"id" : 0,
|
||||
"error" : {
|
||||
"code" : 1001,
|
||||
"message" : "Content root not found"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import akka.testkit.TestProbe
|
||||
import io.circe.literal._
|
||||
import org.enso.languageserver.event.BufferClosed
|
||||
import org.enso.languageserver.filemanager.Path
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.enso.testkit.FlakySpec
|
||||
|
||||
class TextOperationsTest extends BaseServerTest with RetrySpec {
|
||||
class TextOperationsTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
"text/openFile" must {
|
||||
"fail opening a file if it does not exist" taggedAs Retry() in {
|
||||
"fail opening a file if it does not exist" taggedAs Flaky in {
|
||||
// Interaction:
|
||||
// 1. Client tries to open a non-existent file.
|
||||
// 2. Client receives an error message.
|
||||
|
@ -1,9 +1,36 @@
|
||||
package org.enso.searcher
|
||||
package org.enso.polyglot
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
|
||||
|
||||
/** A search suggestion. */
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes(
|
||||
Array(
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Suggestion.Atom],
|
||||
name = "suggestionAtom"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Suggestion.Method],
|
||||
name = "suggestionMethod"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Suggestion.Function],
|
||||
name = "suggestionFunction"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Suggestion.Local],
|
||||
name = "suggestionLocal"
|
||||
)
|
||||
)
|
||||
)
|
||||
sealed trait Suggestion
|
||||
object Suggestion {
|
||||
|
||||
type ExternalId = UUID
|
||||
|
||||
/** The type of a suggestion. */
|
||||
sealed trait Kind
|
||||
object Kind {
|
||||
@ -37,20 +64,32 @@ object Suggestion {
|
||||
defaultValue: Option[String]
|
||||
)
|
||||
|
||||
/** Position in the text.
|
||||
*
|
||||
* @param line a line position in a document (zero-based).
|
||||
* @param character a character offset
|
||||
*/
|
||||
case class Position(line: Int, character: Int)
|
||||
|
||||
/** The definition scope.
|
||||
*
|
||||
* @param start the start of the definition scope
|
||||
* @param end the end of the definition scope
|
||||
*/
|
||||
case class Scope(start: Int, end: Int)
|
||||
case class Scope(start: Position, end: Position)
|
||||
|
||||
/** A value constructor.
|
||||
*
|
||||
* @param externalId the external id
|
||||
* @param module the module name
|
||||
* @param name the atom name
|
||||
* @param arguments the list of arguments
|
||||
* @param returnType the type of an atom
|
||||
* @param documentation the documentation string
|
||||
*/
|
||||
case class Atom(
|
||||
externalId: Option[ExternalId],
|
||||
module: String,
|
||||
name: String,
|
||||
arguments: Seq[Argument],
|
||||
returnType: String,
|
||||
@ -59,6 +98,8 @@ object Suggestion {
|
||||
|
||||
/** A function defined on a type or a module.
|
||||
*
|
||||
* @param externalId the external id
|
||||
* @param module the module name
|
||||
* @param name the method name
|
||||
* @param arguments the function arguments
|
||||
* @param selfType the self type of a method
|
||||
@ -66,6 +107,8 @@ object Suggestion {
|
||||
* @param documentation the documentation string
|
||||
*/
|
||||
case class Method(
|
||||
externalId: Option[ExternalId],
|
||||
module: String,
|
||||
name: String,
|
||||
arguments: Seq[Argument],
|
||||
selfType: String,
|
||||
@ -75,12 +118,16 @@ object Suggestion {
|
||||
|
||||
/** A local function definition.
|
||||
*
|
||||
* @param externalId the external id
|
||||
* @param module the module name
|
||||
* @param name the function name
|
||||
* @param arguments the function arguments
|
||||
* @param returnType the return type of a function
|
||||
* @param scope the scope where the function is defined
|
||||
*/
|
||||
case class Function(
|
||||
externalId: Option[ExternalId],
|
||||
module: String,
|
||||
name: String,
|
||||
arguments: Seq[Argument],
|
||||
returnType: String,
|
||||
@ -89,10 +136,17 @@ object Suggestion {
|
||||
|
||||
/** A local value.
|
||||
*
|
||||
* @param externalId the external id
|
||||
* @param module the module name
|
||||
* @param name the name of a value
|
||||
* @param returnType the type of a local value
|
||||
* @param scope the scope where the value is defined
|
||||
*/
|
||||
case class Local(name: String, returnType: String, scope: Scope)
|
||||
extends Suggestion
|
||||
case class Local(
|
||||
externalId: Option[ExternalId],
|
||||
module: String,
|
||||
name: String,
|
||||
returnType: String,
|
||||
scope: Scope
|
||||
) extends Suggestion
|
||||
}
|
@ -11,7 +11,7 @@ import com.fasterxml.jackson.module.scala.{
|
||||
DefaultScalaModule,
|
||||
ScalaObjectMapper
|
||||
}
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
|
||||
import scala.util.Try
|
||||
@ -163,6 +163,10 @@ object Runtime {
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.SuggestionsDatabaseUpdateNotification],
|
||||
name = "suggestionsDatabaseUpdateNotification"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.SuggestionsDatabaseReIndexNotification],
|
||||
name = "suggestionsDatabaseReindexNotification"
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -565,9 +569,13 @@ object Runtime {
|
||||
*
|
||||
* @param path the file being moved to memory.
|
||||
* @param contents the current file contents.
|
||||
* @param isIndexed the flag specifying whether the file is indexed
|
||||
*/
|
||||
case class OpenFileNotification(path: File, contents: String)
|
||||
extends ApiRequest
|
||||
case class OpenFileNotification(
|
||||
path: File,
|
||||
contents: String,
|
||||
isIndexed: Boolean
|
||||
) extends ApiRequest
|
||||
|
||||
/**
|
||||
* A notification sent to the server about in-memory file contents being
|
||||
@ -672,8 +680,10 @@ object Runtime {
|
||||
|
||||
/**
|
||||
* Signals that project has been renamed.
|
||||
*
|
||||
* @param newName the new project name
|
||||
*/
|
||||
case class ProjectRenamed() extends ApiResponse
|
||||
case class ProjectRenamed(newName: String) extends ApiResponse
|
||||
|
||||
/**
|
||||
* A notification about the change in the suggestions database.
|
||||
@ -684,6 +694,17 @@ object Runtime {
|
||||
updates: Seq[SuggestionsDatabaseUpdate]
|
||||
) extends ApiNotification
|
||||
|
||||
/**
|
||||
* A notification about the re-indexed module updates.
|
||||
*
|
||||
* @param moduleName the name of re-indexed module
|
||||
* @param updates the list of database updates
|
||||
*/
|
||||
case class SuggestionsDatabaseReIndexNotification(
|
||||
moduleName: String,
|
||||
updates: Seq[SuggestionsDatabaseUpdate.Add]
|
||||
) extends ApiNotification
|
||||
|
||||
private lazy val mapper = {
|
||||
val factory = new CBORFactory()
|
||||
val mapper = new ObjectMapper(factory) with ScalaObjectMapper
|
||||
|
@ -37,6 +37,7 @@ public class Module implements TruffleObject {
|
||||
private TruffleFile sourceFile;
|
||||
private Rope literalSource;
|
||||
private boolean isParsed = false;
|
||||
private boolean isIndexed = false;
|
||||
private IR ir;
|
||||
private final QualifiedName name;
|
||||
|
||||
@ -217,6 +218,16 @@ public class Module implements TruffleObject {
|
||||
}
|
||||
}
|
||||
|
||||
/** @return the indexed flag. */
|
||||
public boolean isIndexed() {
|
||||
return isIndexed;
|
||||
}
|
||||
|
||||
/** Set the indexed flag. */
|
||||
public void setIndexed(boolean indexed) {
|
||||
isIndexed = indexed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles member invocations through the polyglot API.
|
||||
*
|
||||
|
@ -17,7 +17,10 @@ import org.enso.interpreter.runtime.scope.ModuleScope;
|
||||
import org.enso.polyglot.LanguageInfo;
|
||||
import org.enso.polyglot.MethodNames;
|
||||
import org.enso.text.buffer.Rope;
|
||||
import org.enso.text.editing.*;
|
||||
import org.enso.text.editing.IndexedSource;
|
||||
import org.enso.text.editing.JavaEditorAdapter;
|
||||
import org.enso.text.editing.TextEditor;
|
||||
import org.enso.text.editing.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
@ -184,12 +187,16 @@ public class ExecutionService {
|
||||
* @param path the module path.
|
||||
* @param contents the sources to use for it.
|
||||
*/
|
||||
public void setModuleSources(File path, String contents) {
|
||||
public void setModuleSources(File path, String contents, boolean isIndexed) {
|
||||
Optional<Module> module = context.getModuleForFile(path);
|
||||
if (!module.isPresent()) {
|
||||
module = context.createModuleForFile(path);
|
||||
}
|
||||
module.ifPresent(mod -> mod.setLiteralSource(contents));
|
||||
module.ifPresent(
|
||||
mod -> {
|
||||
mod.setLiteralSource(contents);
|
||||
mod.setIndexed(isIndexed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,7 +49,7 @@ class Compiler(private val context: Context) {
|
||||
val compilerOutput = runCompilerPhases(expr, moduleContext)
|
||||
runErrorHandling(compilerOutput, source, moduleContext)
|
||||
truffleCodegen(compilerOutput, source, scope)
|
||||
expr
|
||||
compilerOutput
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,9 +15,9 @@ import scala.collection.mutable
|
||||
*
|
||||
* @param source the text source
|
||||
* @param ir the IR node
|
||||
* @tparam A a source type
|
||||
* @tparam A the source type
|
||||
*/
|
||||
final class Changeset[A: TextEditor: IndexedSource](val source: A, ir: IR) {
|
||||
final class Changeset[A: TextEditor: IndexedSource](val source: A, val ir: IR) {
|
||||
|
||||
/** Traverses the IR and returns a list of all IR nodes affected by the edit
|
||||
* using the [[DataflowAnalysis]] information.
|
||||
@ -67,6 +67,14 @@ final class Changeset[A: TextEditor: IndexedSource](val source: A, ir: IR) {
|
||||
go(tree, source, mutable.Queue.from(edits), mutable.HashSet())
|
||||
}
|
||||
|
||||
/** Apply the list of edits to the source file.
|
||||
*
|
||||
* @param edits the text edits
|
||||
* @return the source file after applying the edits
|
||||
*/
|
||||
def applyEdits(edits: Iterable[TextEdit]): A =
|
||||
edits.foldLeft(source)(TextEditor[A].edit)
|
||||
|
||||
}
|
||||
|
||||
object Changeset {
|
||||
|
@ -2,23 +2,29 @@ package org.enso.compiler.context
|
||||
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.pass.resolve.{DocumentationComments, TypeSignatures}
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.syntax.text.Location
|
||||
import org.enso.text.editing.IndexedSource
|
||||
|
||||
import scala.collection.immutable.VectorBuilder
|
||||
import scala.collection.mutable
|
||||
|
||||
/** Module that extracts [[Suggestion]] entries from the [[IR]]. */
|
||||
final class SuggestionBuilder {
|
||||
/** Module that extracts [[Suggestion]] entries from the [[IR]].
|
||||
*
|
||||
* @param source the text source
|
||||
* @tparam A the type of the text source
|
||||
*/
|
||||
final class SuggestionBuilder[A: IndexedSource](val source: A) {
|
||||
|
||||
import SuggestionBuilder._
|
||||
|
||||
/** Build suggestions from the given `ir`.
|
||||
*
|
||||
* @param module the module name
|
||||
* @param ir the input `IR`
|
||||
* @return the list of suggestion entries extracted from the given `IR`
|
||||
*/
|
||||
def build(ir: IR.Module): Vector[Suggestion] = {
|
||||
def build(module: String, ir: IR): Vector[Suggestion] = {
|
||||
@scala.annotation.tailrec
|
||||
def go(
|
||||
scope: Scope,
|
||||
@ -38,35 +44,61 @@ final class SuggestionBuilder {
|
||||
ir match {
|
||||
case IR.Module.Scope.Definition.Method
|
||||
.Explicit(
|
||||
IR.Name.MethodReference(typePtr, methodName, _, _, _),
|
||||
IR.Name.MethodReference(typePtr, methodName, _, _, _),
|
||||
IR.Function.Lambda(args, body, _, _, _, _),
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) =>
|
||||
val typeSignature = ir.getMetadata(TypeSignatures)
|
||||
acc += buildMethod(
|
||||
body.getExternalId,
|
||||
module,
|
||||
methodName,
|
||||
typePtr,
|
||||
args,
|
||||
doc,
|
||||
typeSignature
|
||||
)
|
||||
scopes += Scope(body.children, body.location.map(_.location))
|
||||
go(scope, scopes, acc)
|
||||
case IR.Expression.Binding(
|
||||
name,
|
||||
IR.Function.Lambda(args, body, _, _, _, _),
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) =>
|
||||
val typeSignature = ir.getMetadata(TypeSignatures)
|
||||
acc += buildMethod(methodName, typePtr, args, doc, typeSignature)
|
||||
scopes += Scope(body.children, body.location.map(_.location))
|
||||
go(scope, scopes, acc)
|
||||
case IR.Expression.Binding(
|
||||
name,
|
||||
IR.Function.Lambda(args, body, _, _, _, _),
|
||||
_,
|
||||
_,
|
||||
_
|
||||
) if name.location.isDefined =>
|
||||
val typeSignature = ir.getMetadata(TypeSignatures)
|
||||
acc += buildFunction(name, args, scope.location.get, typeSignature)
|
||||
acc += buildFunction(
|
||||
body.getExternalId,
|
||||
module,
|
||||
name,
|
||||
args,
|
||||
scope.location.get,
|
||||
typeSignature
|
||||
)
|
||||
scopes += Scope(body.children, body.location.map(_.location))
|
||||
go(scope, scopes, acc)
|
||||
case IR.Expression.Binding(name, expr, _, _, _)
|
||||
if name.location.isDefined =>
|
||||
val typeSignature = ir.getMetadata(TypeSignatures)
|
||||
acc += buildLocal(name.name, scope.location.get, typeSignature)
|
||||
acc += buildLocal(
|
||||
expr.getExternalId,
|
||||
module,
|
||||
name.name,
|
||||
scope.location.get,
|
||||
typeSignature
|
||||
)
|
||||
scopes += Scope(expr.children, expr.location.map(_.location))
|
||||
go(scope, scopes, acc)
|
||||
case IR.Module.Scope.Definition.Atom(name, arguments, _, _, _) =>
|
||||
acc += buildAtom(name.name, arguments, doc)
|
||||
acc += buildAtom(
|
||||
module,
|
||||
name.name,
|
||||
arguments,
|
||||
doc
|
||||
)
|
||||
go(scope, scopes, acc)
|
||||
case _ =>
|
||||
go(scope, scopes, acc)
|
||||
@ -81,6 +113,8 @@ final class SuggestionBuilder {
|
||||
}
|
||||
|
||||
private def buildMethod(
|
||||
externalId: Option[IR.ExternalId],
|
||||
module: String,
|
||||
name: IR.Name,
|
||||
typeRef: Seq[IR.Name],
|
||||
args: Seq[IR.DefinitionArgument],
|
||||
@ -94,6 +128,8 @@ final class SuggestionBuilder {
|
||||
val (methodArgs, returnTypeDef) =
|
||||
buildMethodArguments(args, typeSig, selfType)
|
||||
Suggestion.Method(
|
||||
externalId = externalId,
|
||||
module = module,
|
||||
name = name.name,
|
||||
arguments = methodArgs,
|
||||
selfType = selfType,
|
||||
@ -102,6 +138,8 @@ final class SuggestionBuilder {
|
||||
)
|
||||
case _ =>
|
||||
Suggestion.Method(
|
||||
externalId = externalId,
|
||||
module = module,
|
||||
name = name.name,
|
||||
arguments = args.map(buildArgument),
|
||||
selfType = buildSelfType(typeRef),
|
||||
@ -112,6 +150,8 @@ final class SuggestionBuilder {
|
||||
}
|
||||
|
||||
private def buildFunction(
|
||||
externalId: Option[IR.ExternalId],
|
||||
module: String,
|
||||
name: IR.Name,
|
||||
args: Seq[IR.DefinitionArgument],
|
||||
location: Location,
|
||||
@ -123,6 +163,8 @@ final class SuggestionBuilder {
|
||||
val (methodArgs, returnTypeDef) =
|
||||
buildFunctionArguments(args, typeSig)
|
||||
Suggestion.Function(
|
||||
externalId = externalId,
|
||||
module = module,
|
||||
name = name.name,
|
||||
arguments = methodArgs,
|
||||
returnType = buildReturnType(returnTypeDef),
|
||||
@ -130,6 +172,8 @@ final class SuggestionBuilder {
|
||||
)
|
||||
case _ =>
|
||||
Suggestion.Function(
|
||||
externalId = externalId,
|
||||
module = module,
|
||||
name = name.name,
|
||||
arguments = args.map(buildArgument),
|
||||
returnType = Any,
|
||||
@ -139,23 +183,34 @@ final class SuggestionBuilder {
|
||||
}
|
||||
|
||||
private def buildLocal(
|
||||
externalId: Option[IR.ExternalId],
|
||||
module: String,
|
||||
name: String,
|
||||
location: Location,
|
||||
typeSignature: Option[TypeSignatures.Metadata]
|
||||
): Suggestion.Local =
|
||||
typeSignature match {
|
||||
case Some(TypeSignatures.Signature(tname: IR.Name)) =>
|
||||
Suggestion.Local(name, tname.name, buildScope(location))
|
||||
Suggestion.Local(
|
||||
externalId,
|
||||
module,
|
||||
name,
|
||||
tname.name,
|
||||
buildScope(location)
|
||||
)
|
||||
case _ =>
|
||||
Suggestion.Local(name, Any, buildScope(location))
|
||||
Suggestion.Local(externalId, module, name, Any, buildScope(location))
|
||||
}
|
||||
|
||||
private def buildAtom(
|
||||
module: String,
|
||||
name: String,
|
||||
arguments: Seq[IR.DefinitionArgument],
|
||||
doc: Option[String]
|
||||
): Suggestion.Atom =
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = module,
|
||||
name = name,
|
||||
arguments = arguments.map(buildArgument),
|
||||
returnType = name,
|
||||
@ -299,11 +354,24 @@ final class SuggestionBuilder {
|
||||
}
|
||||
|
||||
private def buildScope(location: Location): Suggestion.Scope =
|
||||
Suggestion.Scope(location.start, location.end)
|
||||
Suggestion.Scope(toPosition(location.start), toPosition(location.end))
|
||||
|
||||
private def toPosition(index: Int): Suggestion.Position = {
|
||||
val pos = IndexedSource[A].toPosition(index, source)
|
||||
Suggestion.Position(pos.line, pos.character)
|
||||
}
|
||||
}
|
||||
|
||||
object SuggestionBuilder {
|
||||
|
||||
/** Create the suggestion builder.
|
||||
*
|
||||
* @param source the text source
|
||||
* @tparam A the type of the text source
|
||||
*/
|
||||
def apply[A: IndexedSource](source: A): SuggestionBuilder[A] =
|
||||
new SuggestionBuilder[A](source)
|
||||
|
||||
/** A single level of an `IR`.
|
||||
*
|
||||
* @param queue the nodes in the scope
|
||||
|
@ -58,7 +58,8 @@ class PassManager(
|
||||
/** Calculates the number of times each pass occurs in the pass ordering.
|
||||
*
|
||||
* @return the a mapping from the pass identifier to the number of times the
|
||||
* pass occurs */
|
||||
* pass occurs
|
||||
*/
|
||||
private def calculatePassCounts: mutable.Map[UUID, PassCount] = {
|
||||
val passCounts: mutable.Map[UUID, PassCount] = mutable.Map()
|
||||
|
||||
|
@ -12,16 +12,20 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
*/
|
||||
class OpenFileCmd(request: Api.OpenFileNotification) extends Command(None) {
|
||||
|
||||
/** @inheritdoc **/
|
||||
override def execute(
|
||||
implicit ctx: RuntimeContext,
|
||||
/** @inheritdoc */
|
||||
override def execute(implicit
|
||||
ctx: RuntimeContext,
|
||||
ec: ExecutionContext
|
||||
): Future[Unit] =
|
||||
Future {
|
||||
ctx.locking.acquireFileLock(request.path)
|
||||
ctx.locking.acquireReadCompilationLock()
|
||||
try {
|
||||
ctx.executionService.setModuleSources(request.path, request.contents)
|
||||
ctx.executionService.setModuleSources(
|
||||
request.path,
|
||||
request.contents,
|
||||
request.isIndexed
|
||||
)
|
||||
} finally {
|
||||
ctx.locking.releaseReadCompilationLock()
|
||||
ctx.locking.releaseFileLock(request.path)
|
||||
|
@ -19,9 +19,9 @@ class RenameProjectCmd(
|
||||
request: Api.RenameProject
|
||||
) extends Command(maybeRequestId) {
|
||||
|
||||
/** @inheritdoc **/
|
||||
override def execute(
|
||||
implicit ctx: RuntimeContext,
|
||||
/** @inheritdoc */
|
||||
override def execute(implicit
|
||||
ctx: RuntimeContext,
|
||||
ec: ExecutionContext
|
||||
): Future[Unit] =
|
||||
Future {
|
||||
@ -34,7 +34,7 @@ class RenameProjectCmd(
|
||||
)
|
||||
val context = ctx.executionService.getContext
|
||||
context.renameProject(request.oldName, request.newName)
|
||||
reply(Api.ProjectRenamed())
|
||||
reply(Api.ProjectRenamed(request.newName))
|
||||
logger.log(Level.INFO, s"Project renamed to ${request.newName}")
|
||||
} finally {
|
||||
ctx.locking.releaseWriteCompilationLock()
|
||||
|
@ -1,10 +1,15 @@
|
||||
package org.enso.interpreter.instrument.job
|
||||
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
|
||||
import org.enso.compiler.context.{Changeset, SuggestionBuilder}
|
||||
import org.enso.interpreter.instrument.CacheInvalidation
|
||||
import org.enso.interpreter.instrument.execution.RuntimeContext
|
||||
import org.enso.interpreter.runtime.Module
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.text.buffer.Rope
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
@ -20,7 +25,7 @@ class EnsureCompiledJob(protected val files: List[File])
|
||||
extends Job[Unit](List.empty, true, false) {
|
||||
|
||||
/**
|
||||
* Ensures that a files is compiled after applying the edits
|
||||
* Create a job ensuring that files are compiled after applying the edits.
|
||||
*
|
||||
* @param file a file to compile
|
||||
*/
|
||||
@ -29,27 +34,63 @@ class EnsureCompiledJob(protected val files: List[File])
|
||||
EnsureCompiledJob.enqueueEdits(file, edits)
|
||||
}
|
||||
|
||||
/** @inheritdoc **/
|
||||
/** @inheritdoc */
|
||||
override def run(implicit ctx: RuntimeContext): Unit = {
|
||||
ctx.locking.acquireWriteCompilationLock()
|
||||
try {
|
||||
val modules = files.flatMap(compile)
|
||||
runInvalidation(files)
|
||||
modules.foreach(compile)
|
||||
ensureCompiled(files)
|
||||
} finally {
|
||||
ctx.locking.releaseWriteCompilationLock()
|
||||
}
|
||||
}
|
||||
|
||||
protected def runInvalidation(
|
||||
/**
|
||||
* Run the compilation and invalidation logic.
|
||||
*
|
||||
* @param files the list of files to compile
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
protected def ensureCompiled(
|
||||
files: Iterable[File]
|
||||
)(implicit ctx: RuntimeContext): Unit =
|
||||
runInvalidationCommands {
|
||||
files.flatMap { file =>
|
||||
applyEdits(file, EnsureCompiledJob.dequeueEdits(file))
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
files.foreach { file =>
|
||||
compile(file).foreach { module =>
|
||||
applyEdits(file).ifPresent {
|
||||
case (changeset, edits) =>
|
||||
val moduleName = module.getName.toString
|
||||
runInvalidationCommands(
|
||||
buildCacheInvalidationCommands(changeset, edits)
|
||||
)
|
||||
if (module.isIndexed) {
|
||||
val removedSuggestions = SuggestionBuilder(changeset.source)
|
||||
.build(moduleName, module.getIr)
|
||||
compile(module)
|
||||
val addedSuggestions =
|
||||
SuggestionBuilder(changeset.applyEdits(edits))
|
||||
.build(moduleName, module.getIr)
|
||||
sendSuggestionsUpdateNotification(
|
||||
removedSuggestions diff addedSuggestions,
|
||||
addedSuggestions diff removedSuggestions
|
||||
)
|
||||
} else {
|
||||
val addedSuggestions =
|
||||
SuggestionBuilder(changeset.applyEdits(edits))
|
||||
.build(moduleName, module.getIr)
|
||||
sendReIndexNotification(moduleName, addedSuggestions)
|
||||
module.setIndexed(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the file.
|
||||
*
|
||||
* @param file the file path to compile
|
||||
* @param ctx the runtime context
|
||||
* @return the compiled module
|
||||
*/
|
||||
private def compile(
|
||||
file: File
|
||||
)(implicit ctx: RuntimeContext): Option[Module] = {
|
||||
@ -59,44 +100,76 @@ class EnsureCompiledJob(protected val files: List[File])
|
||||
.toScala
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the module.
|
||||
*
|
||||
* @param module the module to compile.
|
||||
* @param ctx the runtime context
|
||||
* @return the compiled module
|
||||
*/
|
||||
private def compile(module: Module)(implicit ctx: RuntimeContext): Module =
|
||||
module.parseScope(ctx.executionService.getContext).getModule
|
||||
|
||||
private def applyEdits(file: File, edits: Seq[TextEdit])(
|
||||
implicit ctx: RuntimeContext
|
||||
): Iterable[CacheInvalidation] = {
|
||||
/**
|
||||
* Apply pending edits to the file.
|
||||
*
|
||||
* @param file the file to apply edits to
|
||||
* @param ctx the runtime context
|
||||
* @return the [[Changeset]] object and the list of applied edits
|
||||
*/
|
||||
private def applyEdits(
|
||||
file: File
|
||||
)(implicit
|
||||
ctx: RuntimeContext
|
||||
): Optional[(Changeset[Rope], Seq[TextEdit])] = {
|
||||
ctx.locking.acquireFileLock(file)
|
||||
ctx.locking.acquireReadCompilationLock()
|
||||
try {
|
||||
val changesetOpt =
|
||||
ctx.executionService
|
||||
.modifyModuleSources(file, edits.asJava)
|
||||
.toScala
|
||||
val invalidateExpressionsCommand = changesetOpt.map { changeset =>
|
||||
CacheInvalidation.Command.InvalidateKeys(
|
||||
changeset.compute(edits)
|
||||
)
|
||||
}
|
||||
val invalidateStaleCommand = changesetOpt.map { changeset =>
|
||||
val scopeIds = ctx.executionService.getContext.getCompiler
|
||||
.parseMeta(changeset.source.toString)
|
||||
.map(_._2)
|
||||
CacheInvalidation.Command.InvalidateStale(scopeIds)
|
||||
}
|
||||
(invalidateExpressionsCommand.toSeq ++ invalidateStaleCommand.toSeq)
|
||||
.map(
|
||||
CacheInvalidation(
|
||||
CacheInvalidation.StackSelector.All,
|
||||
_,
|
||||
Set(CacheInvalidation.IndexSelector.All)
|
||||
)
|
||||
)
|
||||
val edits = EnsureCompiledJob.dequeueEdits(file)
|
||||
ctx.executionService
|
||||
.modifyModuleSources(file, edits.asJava)
|
||||
.map(_ -> edits)
|
||||
} finally {
|
||||
ctx.locking.releaseReadCompilationLock()
|
||||
ctx.locking.releaseFileLock(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cache invalidation commands after applying the edits.
|
||||
*
|
||||
* @param changeset the [[Changeset]] object capturing the previous version
|
||||
* of IR
|
||||
* @param edits the list of applied edits
|
||||
* @param ctx the runtime context
|
||||
* @return the list of cache invalidation commands
|
||||
*/
|
||||
private def buildCacheInvalidationCommands(
|
||||
changeset: Changeset[Rope],
|
||||
edits: Seq[TextEdit]
|
||||
)(implicit ctx: RuntimeContext): Seq[CacheInvalidation] = {
|
||||
val invalidateExpressionsCommand =
|
||||
CacheInvalidation.Command.InvalidateKeys(changeset.compute(edits))
|
||||
val scopeIds = ctx.executionService.getContext.getCompiler
|
||||
.parseMeta(changeset.source.toString)
|
||||
.map(_._2)
|
||||
val invalidateStaleCommand =
|
||||
CacheInvalidation.Command.InvalidateStale(scopeIds)
|
||||
Seq(invalidateExpressionsCommand, invalidateStaleCommand).map(
|
||||
CacheInvalidation(
|
||||
CacheInvalidation.StackSelector.All,
|
||||
_,
|
||||
Set(CacheInvalidation.IndexSelector.All)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the invalidation commands.
|
||||
*
|
||||
* @param invalidationCommands the invalidation command to run
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
private def runInvalidationCommands(
|
||||
invalidationCommands: Iterable[CacheInvalidation]
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
@ -107,6 +180,47 @@ class EnsureCompiledJob(protected val files: List[File])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about the suggestions database updates.
|
||||
*
|
||||
* @param removed the list of suggestions to remove
|
||||
* @param added the list of suggestions to add
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
private def sendSuggestionsUpdateNotification(
|
||||
removed: Seq[Suggestion],
|
||||
added: Seq[Suggestion]
|
||||
)(implicit ctx: RuntimeContext): Unit =
|
||||
if (added.nonEmpty || removed.nonEmpty) {
|
||||
ctx.endpoint.sendToClient(
|
||||
Api.Response(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
removed.map(Api.SuggestionsDatabaseUpdate.Remove) :++
|
||||
added.map(Api.SuggestionsDatabaseUpdate.Add)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about the re-indexed module updates.
|
||||
*
|
||||
* @param moduleName the name of re-indexed module
|
||||
* @param added the list of suggestions to add
|
||||
* @param ctx the runtime context
|
||||
*/
|
||||
private def sendReIndexNotification(
|
||||
moduleName: String,
|
||||
added: Seq[Suggestion]
|
||||
)(implicit ctx: RuntimeContext): Unit =
|
||||
ctx.endpoint.sendToClient(
|
||||
Api.Response(
|
||||
Api.SuggestionsDatabaseReIndexNotification(
|
||||
moduleName,
|
||||
added.map(Api.SuggestionsDatabaseUpdate.Add)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object EnsureCompiledJob {
|
||||
@ -122,5 +236,4 @@ object EnsureCompiledJob {
|
||||
case Some(v) => Some(v :++ edits)
|
||||
case None => Some(edits)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,11 +17,11 @@ import scala.jdk.OptionConverters._
|
||||
class EnsureCompiledStackJob(stack: Iterable[InstrumentFrame])
|
||||
extends EnsureCompiledJob(EnsureCompiledStackJob.extractFiles(stack)) {
|
||||
|
||||
/** @inheritdoc **/
|
||||
override def runInvalidation(
|
||||
/** @inheritdoc */
|
||||
override def ensureCompiled(
|
||||
files: Iterable[File]
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
super.runInvalidation(files)
|
||||
super.ensureCompiled(files)
|
||||
getCacheMetadata(stack).foreach { metadata =>
|
||||
CacheInvalidation.run(
|
||||
stack,
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.enso.compiler.test.context
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.compiler.Passes
|
||||
import org.enso.compiler.context.{
|
||||
FreshNameSupply,
|
||||
@ -9,7 +11,7 @@ import org.enso.compiler.context.{
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.pass.PassManager
|
||||
import org.enso.compiler.test.CompilerTest
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.enso.polyglot.Suggestion
|
||||
|
||||
class SuggestionBuilderTest extends CompilerTest {
|
||||
|
||||
@ -23,9 +25,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
val code = """foo = 42""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
@ -37,7 +41,6 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
}
|
||||
|
||||
"build method with documentation" in {
|
||||
pending // fix documentation
|
||||
implicit val moduleContext: ModuleContext = freshModuleContext
|
||||
|
||||
val code =
|
||||
@ -45,9 +48,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
|foo = 42""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
@ -69,9 +74,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| x * y""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None),
|
||||
Suggestion.Argument("a", "Any", false, false, None),
|
||||
@ -81,8 +88,20 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
returnType = "Any",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Local("x", "Number", Suggestion.Scope(9, 62)),
|
||||
Suggestion.Local("y", "Any", Suggestion.Scope(9, 62))
|
||||
Suggestion.Local(
|
||||
externalId = None,
|
||||
"Test",
|
||||
"x",
|
||||
"Number",
|
||||
Suggestion.Scope(Suggestion.Position(0, 9), Suggestion.Position(4, 9))
|
||||
),
|
||||
Suggestion.Local(
|
||||
externalId = None,
|
||||
"Test",
|
||||
"y",
|
||||
"Any",
|
||||
Suggestion.Scope(Suggestion.Position(0, 9), Suggestion.Position(4, 9))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -93,9 +112,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
"""foo (a = 0) = a + 1""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None),
|
||||
Suggestion.Argument("a", "Any", false, true, Some("0"))
|
||||
@ -117,9 +138,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
|""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "bar",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "bar",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "MyAtom", false, false, None),
|
||||
Suggestion.Argument("a", "Number", false, false, None),
|
||||
@ -139,9 +162,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
"""foo ~a = a + 1""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None),
|
||||
Suggestion.Argument("a", "Any", true, false, None)
|
||||
@ -162,9 +187,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| foo 42""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "main",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
@ -173,12 +200,17 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Function(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None)
|
||||
),
|
||||
returnType = "Any",
|
||||
scope = Suggestion.Scope(6, 35)
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(0, 6),
|
||||
Suggestion.Position(2, 10)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -193,9 +225,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| foo 42""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
name = "main",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
@ -204,12 +238,17 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Function(
|
||||
name = "foo",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Number", false, false, None)
|
||||
),
|
||||
returnType = "Number",
|
||||
scope = Suggestion.Scope(6, 62)
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(0, 6),
|
||||
Suggestion.Position(3, 10)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -220,9 +259,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
val code = """type MyType a b"""
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
name = "MyType",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "MyType",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None),
|
||||
Suggestion.Argument("b", "Any", false, false, None)
|
||||
@ -241,9 +282,11 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
|type MyType a b""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
name = "MyType",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "MyType",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None),
|
||||
Suggestion.Argument("b", "Any", false, false, None)
|
||||
@ -263,15 +306,19 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| type Just a""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Nothing",
|
||||
arguments = Seq(),
|
||||
returnType = "Nothing",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Atom(
|
||||
name = "Just",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Just",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None)
|
||||
),
|
||||
@ -293,15 +340,19 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| type Just a""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Nothing",
|
||||
arguments = Seq(),
|
||||
returnType = "Nothing",
|
||||
documentation = Some(" Nothing here")
|
||||
),
|
||||
Suggestion.Atom(
|
||||
name = "Just",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Just",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None)
|
||||
),
|
||||
@ -323,15 +374,19 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| Nothing -> Nothing""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Nothing",
|
||||
arguments = Seq(),
|
||||
returnType = "Nothing",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Atom(
|
||||
name = "Just",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "Just",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None)
|
||||
),
|
||||
@ -339,7 +394,9 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Method(
|
||||
name = "map",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "map",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None),
|
||||
Suggestion.Argument("f", "Any", false, false, None)
|
||||
@ -349,7 +406,9 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Method(
|
||||
name = "map",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "map",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None),
|
||||
Suggestion.Argument("f", "Any", false, false, None)
|
||||
@ -371,15 +430,19 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
| is_atom = true""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(module) should contain theSameElementsAs Seq(
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "MyAtom",
|
||||
arguments = Seq(),
|
||||
returnType = "MyAtom",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Method(
|
||||
name = "is_atom",
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "is_atom",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "MyAtom", false, false, None)
|
||||
),
|
||||
@ -389,10 +452,158 @@ class SuggestionBuilderTest extends CompilerTest {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"build module" in {
|
||||
implicit val moduleContext: ModuleContext = freshModuleContext
|
||||
val code =
|
||||
"""type MyType a b
|
||||
|
|
||||
|main = IO.println("Hello!")""".stripMargin
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Atom(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "MyType",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None),
|
||||
Suggestion.Argument("b", "Any", false, false, None)
|
||||
),
|
||||
returnType = "MyType",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Method(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
selfType = "here",
|
||||
returnType = "Any",
|
||||
documentation = None
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"build method with external id" in {
|
||||
implicit val moduleContext: ModuleContext = freshModuleContext
|
||||
val code =
|
||||
"""main = IO.println "Hello!"
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 7}, "size": {"value": 19}}, "4083ce56-a5e5-4ecd-bf45-37ddf0b58456"]]
|
||||
|[]
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
externalId =
|
||||
Some(UUID.fromString("4083ce56-a5e5-4ecd-bf45-37ddf0b58456")),
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
selfType = "here",
|
||||
returnType = "Any",
|
||||
documentation = None
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"build function with external id" in {
|
||||
implicit val moduleContext: ModuleContext = freshModuleContext
|
||||
val code =
|
||||
"""main =
|
||||
| id x = x
|
||||
| IO.println (id "Hello!")
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 18}, "size": {"value": 1}}, "f533d910-63f8-44cd-9204-a1e2d46bb7c3"]]
|
||||
|[]
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
selfType = "here",
|
||||
returnType = "Any",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Function(
|
||||
externalId =
|
||||
Some(UUID.fromString("f533d910-63f8-44cd-9204-a1e2d46bb7c3")),
|
||||
module = "Test",
|
||||
name = "id",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("x", "Any", false, false, None)
|
||||
),
|
||||
returnType = "Any",
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(0, 6),
|
||||
Suggestion.Position(2, 28)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"build local with external id" in {
|
||||
implicit val moduleContext: ModuleContext = freshModuleContext
|
||||
val code =
|
||||
"""main =
|
||||
| foo = 42
|
||||
| IO.println foo
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 17}, "size": {"value": 2}}, "0270bcdf-26b8-4b99-8745-85b3600c7359"]]
|
||||
|[]
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val module = code.preprocessModule
|
||||
|
||||
build(code, module) should contain theSameElementsAs Seq(
|
||||
Suggestion.Method(
|
||||
externalId = None,
|
||||
module = "Test",
|
||||
name = "main",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "Any", false, false, None)
|
||||
),
|
||||
selfType = "here",
|
||||
returnType = "Any",
|
||||
documentation = None
|
||||
),
|
||||
Suggestion.Local(
|
||||
externalId =
|
||||
Some(UUID.fromString("0270bcdf-26b8-4b99-8745-85b3600c7359")),
|
||||
module = "Test",
|
||||
name = "foo",
|
||||
returnType = "Any",
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(0, 6),
|
||||
Suggestion.Position(2, 18)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def build(ir: IR.Module): Vector[Suggestion] =
|
||||
new SuggestionBuilder().build(ir)
|
||||
private val Module = "Test"
|
||||
|
||||
private def build(source: String, ir: IR.Module): Vector[Suggestion] =
|
||||
SuggestionBuilder(source).build(Module, ir)
|
||||
|
||||
private def freshModuleContext: ModuleContext =
|
||||
ModuleContext(freshNameSupply = Some(new FreshNameSupply))
|
||||
|
@ -8,9 +8,8 @@ import java.util.concurrent.{LinkedBlockingQueue, TimeUnit}
|
||||
|
||||
import org.enso.interpreter.test.Metadata
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
import org.enso.polyglot.runtime.Runtime.Api.PushContextResponse
|
||||
import org.enso.polyglot.runtime.Runtime.{Api, ApiRequest}
|
||||
import org.enso.polyglot.{LanguageInfo, PolyglotContext, RuntimeOptions, RuntimeServerInfo}
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.polyglot._
|
||||
import org.enso.text.editing.model
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
import org.graalvm.polyglot.Context
|
||||
@ -377,66 +376,195 @@ class RuntimeServerTest
|
||||
it should "support file modification operations" in {
|
||||
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
|
||||
send(Api.CreateContextRequest(contextId))
|
||||
context.receive
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Create a new file
|
||||
context.writeFile(fooFile, "main = IO.println \"I'm a file!\"")
|
||||
|
||||
// Open the new file
|
||||
send(
|
||||
Api.OpenFileNotification(
|
||||
fooFile,
|
||||
"main = IO.println \"I'm a file!\""
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(
|
||||
fooFile,
|
||||
"main = IO.println \"I'm a file!\"",
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive
|
||||
context.receive shouldEqual None
|
||||
context.consumeOut shouldEqual List()
|
||||
|
||||
// Push new item on the stack to trigger the re-execution
|
||||
send(
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem
|
||||
.ExplicitCall(
|
||||
Api.MethodPointer(fooFile, "Foo", "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem
|
||||
.ExplicitCall(
|
||||
Api.MethodPointer(fooFile, "Foo", "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive(2)
|
||||
context.consumeOut shouldEqual List("I'm a file!")
|
||||
|
||||
// Open the file with contents changed
|
||||
send(
|
||||
Api.OpenFileNotification(
|
||||
fooFile,
|
||||
"main = IO.println \"I'm an open file!\""
|
||||
)
|
||||
)
|
||||
context.receive
|
||||
context.consumeOut shouldEqual List()
|
||||
|
||||
// Modify the file
|
||||
send(
|
||||
Api.EditFileNotification(
|
||||
fooFile,
|
||||
Seq(
|
||||
TextEdit(
|
||||
model.Range(model.Position(0, 24), model.Position(0, 30)),
|
||||
" modified"
|
||||
context.receive(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
Api.SuggestionsDatabaseReIndexNotification(
|
||||
"Test.Foo",
|
||||
Seq(
|
||||
Api.SuggestionsDatabaseUpdate.Add(
|
||||
Suggestion.Method(
|
||||
None,
|
||||
"Test.Foo",
|
||||
"main",
|
||||
Seq(Suggestion.Argument("this", "Any", false, false, None)),
|
||||
"here",
|
||||
"Any",
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive
|
||||
context.consumeOut shouldEqual List("I'm a modified file!")
|
||||
context.consumeOut shouldEqual List("I'm a file!")
|
||||
|
||||
// Modify the file
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.EditFileNotification(
|
||||
fooFile,
|
||||
Seq(
|
||||
TextEdit(
|
||||
model.Range(model.Position(0, 25), model.Position(0, 29)),
|
||||
"modified"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive shouldEqual None
|
||||
context.consumeOut shouldEqual List("I'm a modified!")
|
||||
|
||||
// Close the file
|
||||
send(Api.CloseFileNotification(fooFile))
|
||||
context.receive
|
||||
context.send(Api.Request(Api.CloseFileNotification(fooFile)))
|
||||
context.consumeOut shouldEqual List()
|
||||
}
|
||||
|
||||
it should "send suggestion notifications when file modified" in {
|
||||
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// Create a new file
|
||||
context.writeFile(fooFile, "main = IO.println \"I'm a file!\"")
|
||||
|
||||
// Open the new file
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(
|
||||
fooFile,
|
||||
"main = IO.println \"I'm a file!\"",
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive shouldEqual None
|
||||
context.consumeOut shouldEqual List()
|
||||
|
||||
// Push new item on the stack to trigger the re-execution
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem
|
||||
.ExplicitCall(
|
||||
Api.MethodPointer(fooFile, "Foo", "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
Api.SuggestionsDatabaseReIndexNotification(
|
||||
"Test.Foo",
|
||||
Seq(
|
||||
Api.SuggestionsDatabaseUpdate.Add(
|
||||
Suggestion.Method(
|
||||
None,
|
||||
"Test.Foo",
|
||||
"main",
|
||||
Seq(Suggestion.Argument("this", "Any", false, false, None)),
|
||||
"here",
|
||||
"Any",
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.consumeOut shouldEqual List("I'm a file!")
|
||||
|
||||
// Modify the file
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.EditFileNotification(
|
||||
fooFile,
|
||||
Seq(
|
||||
TextEdit(
|
||||
model.Range(model.Position(0, 25), model.Position(0, 29)),
|
||||
"modified"
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(0, 0), model.Position(0, 0)),
|
||||
"Number.lucky = 42\n\n"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receive(2) should contain theSameElementsAs Seq(
|
||||
Api.Response(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(
|
||||
Api.SuggestionsDatabaseUpdate.Add(
|
||||
Suggestion.Method(
|
||||
None,
|
||||
"Test.Foo",
|
||||
"lucky",
|
||||
Seq(Suggestion.Argument("this", "Any", false, false, None)),
|
||||
"Number",
|
||||
"Any",
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.consumeOut shouldEqual List("I'm a modified!")
|
||||
|
||||
// Close the file
|
||||
context.send(Api.Request(Api.CloseFileNotification(fooFile)))
|
||||
context.receive shouldEqual None
|
||||
context.consumeOut shouldEqual List()
|
||||
}
|
||||
|
||||
@ -587,7 +715,7 @@ class RuntimeServerTest
|
||||
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
|
||||
)
|
||||
context.receive(2) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, PushContextResponse(contextId)),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(Api.ExecutionFailed(contextId, "error in function: main"))
|
||||
)
|
||||
|
||||
@ -645,8 +773,14 @@ class RuntimeServerTest
|
||||
val visualisationFile =
|
||||
context.writeInSrcDir("Visualisation", context.Visualisation.code)
|
||||
|
||||
send(
|
||||
Api.OpenFileNotification(visualisationFile, context.Visualisation.code)
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(
|
||||
visualisationFile,
|
||||
context.Visualisation.code,
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val contextId = UUID.randomUUID()
|
||||
@ -758,8 +892,14 @@ class RuntimeServerTest
|
||||
val visualisationFile =
|
||||
context.writeInSrcDir("Visualisation", context.Visualisation.code)
|
||||
|
||||
send(
|
||||
Api.OpenFileNotification(visualisationFile, context.Visualisation.code)
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(
|
||||
visualisationFile,
|
||||
context.Visualisation.code,
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val contextId = UUID.randomUUID()
|
||||
@ -865,8 +1005,14 @@ class RuntimeServerTest
|
||||
val visualisationFile =
|
||||
context.writeInSrcDir("Visualisation", context.Visualisation.code)
|
||||
|
||||
send(
|
||||
Api.OpenFileNotification(visualisationFile, context.Visualisation.code)
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.OpenFileNotification(
|
||||
visualisationFile,
|
||||
context.Visualisation.code,
|
||||
false
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val contextId = UUID.randomUUID()
|
||||
@ -983,7 +1129,7 @@ class RuntimeServerTest
|
||||
context.pkg.rename("Foo")
|
||||
context.send(Api.Request(requestId, Api.RenameProject("Test", "Foo")))
|
||||
context.receive(1) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.ProjectRenamed())
|
||||
Api.Response(requestId, Api.ProjectRenamed("Foo"))
|
||||
)
|
||||
|
||||
// recompute reusing the cache
|
||||
@ -1011,9 +1157,6 @@ class RuntimeServerTest
|
||||
context.Main.Update.mainY(contextId),
|
||||
context.Main.Update.mainZ(contextId)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private def send(msg: ApiRequest): Unit =
|
||||
context.send(Api.Request(UUID.randomUUID(), msg))
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
lazy val gen = new ObservableGenerator[ZEnv]()
|
||||
|
||||
val testProjectsRoot = Files.createTempDirectory(null).toFile
|
||||
testProjectsRoot.deleteOnExit()
|
||||
sys.addShutdownHook(FileUtils.deleteQuietly(testProjectsRoot))
|
||||
|
||||
val userProjectDir = new File(testProjectsRoot, "projects")
|
||||
|
||||
@ -132,11 +132,7 @@ class BaseServerSpec extends JsonRpcServerTestKit {
|
||||
|
||||
override def afterEach(): Unit = {
|
||||
super.afterEach()
|
||||
try FileUtils.deleteDirectory(testProjectsRoot)
|
||||
catch {
|
||||
case ex: java.io.IOException =>
|
||||
system.log.error(ex, s"Failed to cleanup $testProjectsRoot")
|
||||
}
|
||||
FileUtils.deleteQuietly(testProjectsRoot)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -531,7 +531,7 @@ class ProjectManagementApiSpec
|
||||
deleteProject(projectId)
|
||||
}
|
||||
|
||||
"create a project dir with a suffix if a directory is taken" in {
|
||||
"create a project dir with a suffix if a directory is taken" taggedAs Flaky in {
|
||||
val oldProjectName = "foobar"
|
||||
val newProjectName = "foo"
|
||||
implicit val client = new WsTestClient(address)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.searcher.sql;
|
||||
|
||||
import org.enso.searcher.Suggestion;
|
||||
import org.enso.polyglot.Suggestion;
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
@ -13,6 +13,7 @@ import scala.concurrent.duration.Duration;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Stream;
|
||||
@ -30,12 +31,13 @@ public class SuggestionsRepoBenchmark {
|
||||
|
||||
final Path dbfile = Path.of(System.getProperty("java.io.tmpdir"), "bench-suggestions.db");
|
||||
final Seq<Suggestion.Kind> kinds = SuggestionRandom.nextKinds();
|
||||
final Seq<scala.Tuple2<UUID, String>> updateInput = SuggestionRandom.nextUpdateAllInput();
|
||||
|
||||
SqlSuggestionsRepo repo;
|
||||
|
||||
@Setup
|
||||
public void setup() throws TimeoutException, InterruptedException {
|
||||
repo = SqlSuggestionsRepo.apply(dbfile, ExecutionContext.global());
|
||||
repo = SqlSuggestionsRepo.apply(dbfile.toFile(), ExecutionContext.global());
|
||||
if (Files.notExists(dbfile)) {
|
||||
System.out.println("initializing " + dbfile.toString() + " ...");
|
||||
Await.ready(repo.init(), TIMEOUT);
|
||||
@ -65,33 +67,47 @@ public class SuggestionsRepoBenchmark {
|
||||
|
||||
@Benchmark
|
||||
public Object searchBaseline() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(none(), none(), none()), TIMEOUT);
|
||||
return Await.result(repo.search(none(), none(), none(), none(), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchByReturnType() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(none(), scala.Some.apply("MyType"), none()), TIMEOUT);
|
||||
return Await.result(
|
||||
repo.search(none(), none(), scala.Some.apply("MyType"), none(), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchBySelfType() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(scala.Some.apply("MyType"), none(), none()), TIMEOUT);
|
||||
return Await.result(
|
||||
repo.search(none(), scala.Some.apply("MyType"), none(), none(), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchBySelfReturnTypes() throws TimeoutException, InterruptedException {
|
||||
return Await.result(
|
||||
repo.search(scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), none()), TIMEOUT);
|
||||
repo.search(
|
||||
none(), scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), none(), none()),
|
||||
TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchByAll() throws TimeoutException, InterruptedException {
|
||||
return Await.result(
|
||||
repo.search(
|
||||
scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), scala.Some.apply(kinds)),
|
||||
none(),
|
||||
scala.Some.apply("SelfType"),
|
||||
scala.Some.apply("ReturnType"),
|
||||
scala.Some.apply(kinds),
|
||||
none()),
|
||||
TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object updateByExternalId() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.updateAll(updateInput), TIMEOUT);
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
Options opt =
|
||||
new OptionsBuilder().include(SuggestionsRepoBenchmark.class.getSimpleName()).build();
|
||||
|
@ -1,11 +1,16 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import org.enso.searcher.Suggestion
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.polyglot.Suggestion
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
object SuggestionRandom {
|
||||
|
||||
def nextUpdateAllInput(): Seq[(UUID, String)] =
|
||||
Seq(UUID.randomUUID() -> nextString())
|
||||
|
||||
def nextKinds(): Seq[Suggestion.Kind] =
|
||||
Set.fill(1)(nextKind()).toSeq
|
||||
|
||||
@ -20,6 +25,8 @@ object SuggestionRandom {
|
||||
|
||||
def nextSuggestionAtom(): Suggestion.Atom =
|
||||
Suggestion.Atom(
|
||||
externalId = optional(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = nextString(),
|
||||
arguments = Seq(),
|
||||
returnType = nextString(),
|
||||
@ -28,6 +35,8 @@ object SuggestionRandom {
|
||||
|
||||
def nextSuggestionMethod(): Suggestion.Method =
|
||||
Suggestion.Method(
|
||||
externalId = optional(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = nextString(),
|
||||
arguments = Seq(),
|
||||
selfType = nextString(),
|
||||
@ -37,6 +46,8 @@ object SuggestionRandom {
|
||||
|
||||
def nextSuggestionFunction(): Suggestion.Function =
|
||||
Suggestion.Function(
|
||||
externalId = optional(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = nextString(),
|
||||
arguments = Seq(),
|
||||
returnType = nextString(),
|
||||
@ -45,6 +56,8 @@ object SuggestionRandom {
|
||||
|
||||
def nextSuggestionLocal(): Suggestion.Local =
|
||||
Suggestion.Local(
|
||||
externalId = optional(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = nextString(),
|
||||
returnType = nextString(),
|
||||
scope = nextScope()
|
||||
@ -52,8 +65,14 @@ object SuggestionRandom {
|
||||
|
||||
def nextScope(): Suggestion.Scope =
|
||||
Suggestion.Scope(
|
||||
start = Random.nextInt(Int.MaxValue),
|
||||
end = Random.nextInt(Int.MaxValue)
|
||||
start = nextPosition(),
|
||||
end = nextPosition()
|
||||
)
|
||||
|
||||
def nextPosition(): Suggestion.Position =
|
||||
Suggestion.Position(
|
||||
Random.nextInt(Int.MaxValue),
|
||||
Random.nextInt(Int.MaxValue)
|
||||
)
|
||||
|
||||
def nextKind(): Suggestion.Kind =
|
||||
|
@ -3,5 +3,6 @@ searcher {
|
||||
url = "jdbc:sqlite::memory:"
|
||||
driver = "org.sqlite.JDBC"
|
||||
connectionPool = "HikariCP"
|
||||
properties.journal_mode = "wal"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package org.enso.searcher
|
||||
|
||||
import java.io.File
|
||||
|
||||
/** The object for accessing the database containing the file versions. */
|
||||
trait FileVersionsRepo[F[_]] {
|
||||
|
||||
/** Get the file version.
|
||||
*
|
||||
* @param file the file path
|
||||
* @return the version digest
|
||||
*/
|
||||
def getVersion(file: File): F[Option[Array[Byte]]]
|
||||
|
||||
/** Set the file version.
|
||||
*
|
||||
* @param file the file path
|
||||
* @param digest the version digest
|
||||
* @return previously recorded file version
|
||||
*/
|
||||
def setVersion(file: File, digest: Array[Byte]): F[Option[Array[Byte]]]
|
||||
|
||||
/** Remove the version record.
|
||||
*
|
||||
* @param file the file path
|
||||
*/
|
||||
def remove(file: File): F[Unit]
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package org.enso.searcher
|
||||
|
||||
import org.enso.polyglot.Suggestion
|
||||
|
||||
/** The entry in the suggestions database.
|
||||
*
|
||||
* @param id the suggestion id
|
||||
|
@ -1,14 +1,10 @@
|
||||
package org.enso.searcher
|
||||
|
||||
import org.enso.polyglot.Suggestion
|
||||
|
||||
/** The object for accessing the suggestions database. */
|
||||
trait SuggestionsRepo[F[_]] {
|
||||
|
||||
/** Initialize the repo. */
|
||||
def init: F[Unit]
|
||||
|
||||
/** Clean the repo. */
|
||||
def clean: F[Unit]
|
||||
|
||||
/** Get current version of the repo. */
|
||||
def currentVersion: F[Long]
|
||||
|
||||
@ -20,15 +16,19 @@ trait SuggestionsRepo[F[_]] {
|
||||
|
||||
/** Search suggestion by various parameters.
|
||||
*
|
||||
* @param module the module name search parameter
|
||||
* @param selfType the selfType search parameter
|
||||
* @param returnType the returnType search parameter
|
||||
* @param kinds the list suggestion kinds to search
|
||||
* @param position the absolute position in the text
|
||||
* @return the current database version and the list of found suggestion ids
|
||||
*/
|
||||
def search(
|
||||
module: Option[String],
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
kinds: Option[Seq[Suggestion.Kind]]
|
||||
kinds: Option[Seq[Suggestion.Kind]],
|
||||
position: Option[Suggestion.Position]
|
||||
): F[(Long, Seq[Long])]
|
||||
|
||||
/** Select the suggestion by id.
|
||||
@ -59,10 +59,26 @@ trait SuggestionsRepo[F[_]] {
|
||||
*/
|
||||
def remove(suggestion: Suggestion): F[Option[Long]]
|
||||
|
||||
/** Remove suggestions by module name.
|
||||
*
|
||||
* @param name the module name
|
||||
* @return the current database version and a list of removed suggestion ids
|
||||
*/
|
||||
def removeByModule(name: String): F[(Long, Seq[Long])]
|
||||
|
||||
/** Remove a list of suggestions.
|
||||
*
|
||||
* @param suggestions the suggestions to remove
|
||||
* @return the current database version and a list of removed suggestion ids
|
||||
*/
|
||||
def removeAll(suggestions: Seq[Suggestion]): F[(Long, Seq[Option[Long]])]
|
||||
|
||||
/** Update a list of suggestions by external id.
|
||||
*
|
||||
* @param expressions pairs of external id and a return type
|
||||
* @return the current database version and a list of updated suggestion ids
|
||||
*/
|
||||
def updateAll(
|
||||
expressions: Seq[(Suggestion.ExternalId, String)]
|
||||
): F[(Long, Seq[Option[Long]])]
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.searcher.{Suggestion, SuggestionEntry, SuggestionsRepo}
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.searcher.{SuggestionEntry, SuggestionsRepo}
|
||||
import slick.jdbc.SQLiteProfile.api._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -25,12 +27,12 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
.joinRight(Suggestions)
|
||||
.on(_.suggestionId === _.id)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def init: Future[Unit] =
|
||||
/** Initialize the repo. */
|
||||
def init: Future[Unit] =
|
||||
db.run(initQuery)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def clean: Future[Unit] =
|
||||
/** Clean the repo. */
|
||||
def clean: Future[Unit] =
|
||||
db.run(cleanQuery)
|
||||
|
||||
/** @inheritdoc */
|
||||
@ -39,11 +41,13 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
|
||||
/** @inheritdoc */
|
||||
override def search(
|
||||
module: Option[String],
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
kinds: Option[Seq[Suggestion.Kind]]
|
||||
kinds: Option[Seq[Suggestion.Kind]],
|
||||
position: Option[Suggestion.Position]
|
||||
): Future[(Long, Seq[Long])] =
|
||||
db.run(searchQuery(selfType, returnType, kinds))
|
||||
db.run(searchQuery(module, selfType, returnType, kinds, position))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def select(id: Long): Future[Option[Suggestion]] =
|
||||
@ -63,12 +67,22 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
override def remove(suggestion: Suggestion): Future[Option[Long]] =
|
||||
db.run(removeQuery(suggestion))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def removeByModule(name: String): Future[(Long, Seq[Long])] =
|
||||
db.run(removeByModuleQuery(name))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def removeAll(
|
||||
suggestions: Seq[Suggestion]
|
||||
): Future[(Long, Seq[Option[Long]])] =
|
||||
db.run(removeAllQuery(suggestions))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def updateAll(
|
||||
expressions: Seq[(Suggestion.ExternalId, String)]
|
||||
): Future[(Long, Seq[Option[Long]])] =
|
||||
db.run(updateAllQuery(expressions))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def currentVersion: Future[Long] =
|
||||
db.run(currentVersionQuery)
|
||||
@ -77,23 +91,30 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
def close(): Unit =
|
||||
db.close()
|
||||
|
||||
/** Insert suggestions in a batch. */
|
||||
/** Insert suggestions in a batch.
|
||||
*
|
||||
* @param suggestions the list of suggestions to insert
|
||||
* @return the current database size
|
||||
*/
|
||||
private[sql] def insertBatch(suggestions: Array[Suggestion]): Future[Int] =
|
||||
db.run(insertBatchQuery(suggestions))
|
||||
|
||||
/** The query to initialize the repo. */
|
||||
private def initQuery: DBIO[Unit] =
|
||||
(Suggestions.schema ++ Arguments.schema ++ Versions.schema).createIfNotExists
|
||||
(Suggestions.schema ++ Arguments.schema ++ SuggestionsVersions.schema).createIfNotExists
|
||||
|
||||
/** The query to clean the repo. */
|
||||
private def cleanQuery: DBIO[Unit] =
|
||||
for {
|
||||
_ <- Suggestions.delete
|
||||
_ <- Arguments.delete
|
||||
_ <- Versions.delete
|
||||
_ <- SuggestionsVersions.delete
|
||||
} yield ()
|
||||
|
||||
/** Get all suggestions. */
|
||||
/** Get all suggestions.
|
||||
*
|
||||
* @return the current database version with the list of suggestion entries
|
||||
*/
|
||||
private def getAllQuery: DBIO[(Long, Seq[SuggestionEntry])] = {
|
||||
val query = for {
|
||||
suggestions <- joined.result.map(joinedToSuggestionEntries)
|
||||
@ -104,21 +125,33 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
|
||||
/** The query to search suggestion by various parameters.
|
||||
*
|
||||
* @param module the module name search parameter
|
||||
* @param selfType the selfType search parameter
|
||||
* @param returnType the returnType search parameter
|
||||
* @param kinds the list suggestion kinds to search
|
||||
* @param position the absolute position in the text
|
||||
* @return the list of suggestion ids
|
||||
*/
|
||||
private def searchQuery(
|
||||
module: Option[String],
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
kinds: Option[Seq[Suggestion.Kind]]
|
||||
kinds: Option[Seq[Suggestion.Kind]],
|
||||
position: Option[Suggestion.Position]
|
||||
): DBIO[(Long, Seq[Long])] = {
|
||||
val searchAction =
|
||||
if (selfType.isEmpty && returnType.isEmpty && kinds.isEmpty) {
|
||||
if (
|
||||
module.isEmpty &&
|
||||
selfType.isEmpty &&
|
||||
returnType.isEmpty &&
|
||||
kinds.isEmpty &&
|
||||
position.isEmpty
|
||||
) {
|
||||
DBIO.successful(Seq())
|
||||
} else {
|
||||
val query = searchQueryBuilder(selfType, returnType, kinds).map(_.id)
|
||||
val query =
|
||||
searchQueryBuilder(module, selfType, returnType, kinds, position)
|
||||
.map(_.id)
|
||||
query.result
|
||||
}
|
||||
val query = for {
|
||||
@ -186,8 +219,10 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
val selectQuery = Suggestions
|
||||
.filter(_.kind === raw.kind)
|
||||
.filter(_.name === raw.name)
|
||||
.filter(_.scopeStart === raw.scopeStart)
|
||||
.filter(_.scopeEnd === raw.scopeEnd)
|
||||
.filter(_.scopeStartLine === raw.scopeStartLine)
|
||||
.filter(_.scopeStartOffset === raw.scopeStartOffset)
|
||||
.filter(_.scopeEndLine === raw.scopeEndLine)
|
||||
.filter(_.scopeEndOffset === raw.scopeEndOffset)
|
||||
val deleteQuery = for {
|
||||
rows <- selectQuery.result
|
||||
n <- selectQuery.delete
|
||||
@ -196,6 +231,21 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
deleteQuery.transactionally
|
||||
}
|
||||
|
||||
/** The query to remove the suggestions by module name
|
||||
*
|
||||
* @param name the module name
|
||||
* @return the current database version and a list of removed suggestion ids
|
||||
*/
|
||||
private def removeByModuleQuery(name: String): DBIO[(Long, Seq[Long])] = {
|
||||
val selectQuery = Suggestions.filter(_.module === name)
|
||||
val deleteQuery = for {
|
||||
rows <- selectQuery.result
|
||||
n <- selectQuery.delete
|
||||
version <- if (n > 0) incrementVersionQuery else currentVersionQuery
|
||||
} yield version -> rows.flatMap(_.id)
|
||||
deleteQuery.transactionally
|
||||
}
|
||||
|
||||
/** The query to remove a list of suggestions.
|
||||
*
|
||||
* @param suggestions the suggestions to remove
|
||||
@ -211,23 +261,67 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
query.transactionally
|
||||
}
|
||||
|
||||
/** The query to update a suggestion.
|
||||
*
|
||||
* @param externalId the external id of a suggestion
|
||||
* @param returnType the new return type
|
||||
* @return the id of updated suggestion
|
||||
*/
|
||||
private def updateQuery(
|
||||
externalId: Suggestion.ExternalId,
|
||||
returnType: String
|
||||
): DBIO[Option[Long]] = {
|
||||
val selectQuery = Suggestions
|
||||
.filter { row =>
|
||||
row.externalIdLeast === externalId.getLeastSignificantBits &&
|
||||
row.externalIdMost === externalId.getMostSignificantBits
|
||||
}
|
||||
for {
|
||||
id <- selectQuery.map(_.id).result.headOption
|
||||
_ <- selectQuery.map(_.returnType).update(returnType)
|
||||
} yield id
|
||||
}
|
||||
|
||||
/** The query to update a list of suggestions by external id.
|
||||
*
|
||||
* @param expressions the list of expressions to update
|
||||
* @return the current database version with the list of updated suggestion ids
|
||||
*/
|
||||
private def updateAllQuery(
|
||||
expressions: Seq[(Suggestion.ExternalId, String)]
|
||||
): DBIO[(Long, Seq[Option[Long]])] = {
|
||||
val query = for {
|
||||
ids <- DBIO.sequence(expressions.map(Function.tupled(updateQuery)))
|
||||
version <-
|
||||
if (ids.exists(_.nonEmpty)) incrementVersionQuery
|
||||
else currentVersionQuery
|
||||
} yield (version, ids)
|
||||
query.transactionally
|
||||
}
|
||||
|
||||
/** The query to get current version of the repo. */
|
||||
private def currentVersionQuery: DBIO[Long] = {
|
||||
for {
|
||||
versionOpt <- Versions.result.headOption
|
||||
versionOpt <- SuggestionsVersions.result.headOption
|
||||
} yield versionOpt.flatMap(_.id).getOrElse(0L)
|
||||
}
|
||||
|
||||
/** The query to increment the current version of the repo. */
|
||||
private def incrementVersionQuery: DBIO[Long] = {
|
||||
val increment = for {
|
||||
version <- Versions.returning(Versions.map(_.id)) += VersionRow(None)
|
||||
_ <- Versions.filterNot(_.id === version).delete
|
||||
version <- SuggestionsVersions.returning(
|
||||
SuggestionsVersions.map(_.id)
|
||||
) += SuggestionsVersionRow(None)
|
||||
_ <- SuggestionsVersions.filterNot(_.id === version).delete
|
||||
} yield version
|
||||
increment.transactionally
|
||||
}
|
||||
|
||||
/** The query to insert suggestions in a batch. */
|
||||
/** The query to insert suggestions in a batch.
|
||||
*
|
||||
* @param suggestions the list of suggestions to insert
|
||||
* @return the current size of the database
|
||||
*/
|
||||
private def insertBatchQuery(
|
||||
suggestions: Array[Suggestion]
|
||||
): DBIO[Int] = {
|
||||
@ -238,13 +332,26 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
} yield size
|
||||
}
|
||||
|
||||
/** Create a search query by the provided parameters. */
|
||||
/** Create a search query by the provided parameters.
|
||||
*
|
||||
* @param module the module name search parameter
|
||||
* @param selfType the selfType search parameter
|
||||
* @param returnType the returnType search parameter
|
||||
* @param kinds the list suggestion kinds to search
|
||||
* @param position the absolute position in the text
|
||||
* @return the search query
|
||||
*/
|
||||
private def searchQueryBuilder(
|
||||
module: Option[String],
|
||||
selfType: Option[String],
|
||||
returnType: Option[String],
|
||||
kinds: Option[Seq[Suggestion.Kind]]
|
||||
kinds: Option[Seq[Suggestion.Kind]],
|
||||
position: Option[Suggestion.Position]
|
||||
): Query[SuggestionsTable, SuggestionRow, Seq] = {
|
||||
Suggestions
|
||||
.filterOpt(module) {
|
||||
case (row, value) => row.module === value
|
||||
}
|
||||
.filterOpt(selfType) {
|
||||
case (row, value) => row.selfType === value
|
||||
}
|
||||
@ -254,6 +361,16 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
.filterOpt(kinds) {
|
||||
case (row, value) => row.kind inSet value.map(SuggestionKind(_))
|
||||
}
|
||||
.filterOpt(position) {
|
||||
case (row, value) =>
|
||||
(row.scopeStartLine === ScopeColumn.EMPTY) ||
|
||||
(
|
||||
row.scopeStartLine <= value.line &&
|
||||
row.scopeStartOffset <= value.character &&
|
||||
row.scopeEndLine >= value.line &&
|
||||
row.scopeEndOffset >= value.character
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert the rows of suggestions joined with arguments to a list of
|
||||
@ -289,52 +406,80 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
suggestion: Suggestion
|
||||
): (SuggestionRow, Seq[Suggestion.Argument]) =
|
||||
suggestion match {
|
||||
case Suggestion.Atom(name, args, returnType, doc) =>
|
||||
case Suggestion.Atom(expr, module, name, args, returnType, doc) =>
|
||||
val row = SuggestionRow(
|
||||
id = None,
|
||||
kind = SuggestionKind.ATOM,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = doc,
|
||||
scopeStart = ScopeColumn.EMPTY,
|
||||
scopeEnd = ScopeColumn.EMPTY
|
||||
id = None,
|
||||
externalIdLeast = expr.map(_.getLeastSignificantBits),
|
||||
externalIdMost = expr.map(_.getMostSignificantBits),
|
||||
kind = SuggestionKind.ATOM,
|
||||
module = module,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = doc,
|
||||
scopeStartLine = ScopeColumn.EMPTY,
|
||||
scopeStartOffset = ScopeColumn.EMPTY,
|
||||
scopeEndLine = ScopeColumn.EMPTY,
|
||||
scopeEndOffset = ScopeColumn.EMPTY
|
||||
)
|
||||
row -> args
|
||||
case Suggestion.Method(name, args, selfType, returnType, doc) =>
|
||||
case Suggestion.Method(
|
||||
expr,
|
||||
module,
|
||||
name,
|
||||
args,
|
||||
selfType,
|
||||
returnType,
|
||||
doc
|
||||
) =>
|
||||
val row = SuggestionRow(
|
||||
id = None,
|
||||
kind = SuggestionKind.METHOD,
|
||||
name = name,
|
||||
selfType = selfType,
|
||||
returnType = returnType,
|
||||
documentation = doc,
|
||||
scopeStart = ScopeColumn.EMPTY,
|
||||
scopeEnd = ScopeColumn.EMPTY
|
||||
id = None,
|
||||
externalIdLeast = expr.map(_.getLeastSignificantBits),
|
||||
externalIdMost = expr.map(_.getMostSignificantBits),
|
||||
kind = SuggestionKind.METHOD,
|
||||
module = module,
|
||||
name = name,
|
||||
selfType = selfType,
|
||||
returnType = returnType,
|
||||
documentation = doc,
|
||||
scopeStartLine = ScopeColumn.EMPTY,
|
||||
scopeStartOffset = ScopeColumn.EMPTY,
|
||||
scopeEndLine = ScopeColumn.EMPTY,
|
||||
scopeEndOffset = ScopeColumn.EMPTY
|
||||
)
|
||||
row -> args
|
||||
case Suggestion.Function(name, args, returnType, scope) =>
|
||||
case Suggestion.Function(expr, module, name, args, returnType, scope) =>
|
||||
val row = SuggestionRow(
|
||||
id = None,
|
||||
kind = SuggestionKind.FUNCTION,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = None,
|
||||
scopeStart = scope.start,
|
||||
scopeEnd = scope.end
|
||||
id = None,
|
||||
externalIdLeast = expr.map(_.getLeastSignificantBits),
|
||||
externalIdMost = expr.map(_.getMostSignificantBits),
|
||||
kind = SuggestionKind.FUNCTION,
|
||||
module = module,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = None,
|
||||
scopeStartLine = scope.start.line,
|
||||
scopeStartOffset = scope.start.character,
|
||||
scopeEndLine = scope.end.line,
|
||||
scopeEndOffset = scope.end.character
|
||||
)
|
||||
row -> args
|
||||
case Suggestion.Local(name, returnType, scope) =>
|
||||
case Suggestion.Local(expr, module, name, returnType, scope) =>
|
||||
val row = SuggestionRow(
|
||||
id = None,
|
||||
kind = SuggestionKind.LOCAL,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = None,
|
||||
scopeStart = scope.start,
|
||||
scopeEnd = scope.end
|
||||
id = None,
|
||||
externalIdLeast = expr.map(_.getLeastSignificantBits),
|
||||
externalIdMost = expr.map(_.getMostSignificantBits),
|
||||
kind = SuggestionKind.LOCAL,
|
||||
module = module,
|
||||
name = name,
|
||||
selfType = SelfTypeColumn.EMPTY,
|
||||
returnType = returnType,
|
||||
documentation = None,
|
||||
scopeStartLine = scope.start.line,
|
||||
scopeStartOffset = scope.start.character,
|
||||
scopeEndLine = scope.end.line,
|
||||
scopeEndOffset = scope.end.character
|
||||
)
|
||||
row -> Seq()
|
||||
}
|
||||
@ -371,6 +516,9 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
suggestion.kind match {
|
||||
case SuggestionKind.ATOM =>
|
||||
Suggestion.Atom(
|
||||
externalId =
|
||||
toUUID(suggestion.externalIdLeast, suggestion.externalIdMost),
|
||||
module = suggestion.module,
|
||||
name = suggestion.name,
|
||||
arguments = arguments.sortBy(_.index).map(toArgument),
|
||||
returnType = suggestion.returnType,
|
||||
@ -378,6 +526,9 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
)
|
||||
case SuggestionKind.METHOD =>
|
||||
Suggestion.Method(
|
||||
externalId =
|
||||
toUUID(suggestion.externalIdLeast, suggestion.externalIdMost),
|
||||
module = suggestion.module,
|
||||
name = suggestion.name,
|
||||
arguments = arguments.sortBy(_.index).map(toArgument),
|
||||
selfType = suggestion.selfType,
|
||||
@ -386,16 +537,40 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
)
|
||||
case SuggestionKind.FUNCTION =>
|
||||
Suggestion.Function(
|
||||
externalId =
|
||||
toUUID(suggestion.externalIdLeast, suggestion.externalIdMost),
|
||||
module = suggestion.module,
|
||||
name = suggestion.name,
|
||||
arguments = arguments.sortBy(_.index).map(toArgument),
|
||||
returnType = suggestion.returnType,
|
||||
scope = Suggestion.Scope(suggestion.scopeStart, suggestion.scopeEnd)
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(
|
||||
suggestion.scopeStartLine,
|
||||
suggestion.scopeStartOffset
|
||||
),
|
||||
Suggestion.Position(
|
||||
suggestion.scopeEndLine,
|
||||
suggestion.scopeEndOffset
|
||||
)
|
||||
)
|
||||
)
|
||||
case SuggestionKind.LOCAL =>
|
||||
Suggestion.Local(
|
||||
externalId =
|
||||
toUUID(suggestion.externalIdLeast, suggestion.externalIdMost),
|
||||
module = suggestion.module,
|
||||
name = suggestion.name,
|
||||
returnType = suggestion.returnType,
|
||||
scope = Suggestion.Scope(suggestion.scopeStart, suggestion.scopeEnd)
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(
|
||||
suggestion.scopeStartLine,
|
||||
suggestion.scopeStartOffset
|
||||
),
|
||||
Suggestion.Position(
|
||||
suggestion.scopeEndLine,
|
||||
suggestion.scopeEndOffset
|
||||
)
|
||||
)
|
||||
)
|
||||
case k =>
|
||||
throw new NoSuchElementException(s"Unknown suggestion kind: $k")
|
||||
@ -410,6 +585,18 @@ final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
|
||||
hasDefault = row.hasDefault,
|
||||
defaultValue = row.defaultValue
|
||||
)
|
||||
|
||||
/** Convert bits to the UUID.
|
||||
*
|
||||
* @param least the least significant bits of the UUID
|
||||
* @param most the most significant bits of the UUID
|
||||
* @return the new UUID
|
||||
*/
|
||||
private def toUUID(least: Option[Long], most: Option[Long]): Option[UUID] =
|
||||
for {
|
||||
l <- least
|
||||
m <- most
|
||||
} yield new UUID(m, l)
|
||||
}
|
||||
|
||||
object SqlSuggestionsRepo {
|
||||
@ -427,7 +614,7 @@ object SqlSuggestionsRepo {
|
||||
* @param path the path to the database file.
|
||||
* @return the suggestions repo backed up by SQL database.
|
||||
*/
|
||||
def apply(path: Path)(implicit ec: ExecutionContext): SqlSuggestionsRepo = {
|
||||
def apply(path: File)(implicit ec: ExecutionContext): SqlSuggestionsRepo = {
|
||||
new SqlSuggestionsRepo(SqlDatabase(path.toString))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,113 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import java.io.File
|
||||
|
||||
import org.enso.searcher.FileVersionsRepo
|
||||
import slick.jdbc.SQLiteProfile.api._
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
final class SqlVersionsRepo private (db: SqlDatabase)(implicit
|
||||
ec: ExecutionContext
|
||||
) extends FileVersionsRepo[Future] {
|
||||
|
||||
/** Initialize the repo. */
|
||||
def init: Future[Unit] =
|
||||
db.run(initQuery)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getVersion(file: File): Future[Option[Array[Byte]]] =
|
||||
db.run(getVersionQuery(file))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def setVersion(
|
||||
file: File,
|
||||
digest: Array[Byte]
|
||||
): Future[Option[Array[Byte]]] =
|
||||
db.run(setVersionQuery(file, digest))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def remove(file: File): Future[Unit] =
|
||||
db.run(removeQuery(file))
|
||||
|
||||
/** Clean the database. */
|
||||
def clean: Future[Unit] =
|
||||
db.run(cleanQuery)
|
||||
|
||||
/** Close the database. */
|
||||
def close(): Unit =
|
||||
db.close()
|
||||
|
||||
/** The query to initialize the repo. */
|
||||
private def initQuery: DBIO[Unit] =
|
||||
FileVersions.schema.createIfNotExists
|
||||
|
||||
/** The query to clean the repo. */
|
||||
private def cleanQuery: DBIO[Unit] =
|
||||
FileVersions.delete >> DBIO.successful(())
|
||||
|
||||
/** The query to get the version digest of the file.
|
||||
*
|
||||
* @param file the file path
|
||||
* @return the version digest
|
||||
*/
|
||||
private def getVersionQuery(file: File): DBIO[Option[Array[Byte]]] = {
|
||||
val query = for {
|
||||
row <- FileVersions
|
||||
if row.path === file.toString
|
||||
} yield row.digest
|
||||
query.result.headOption
|
||||
}
|
||||
|
||||
/** The query to set the version digest of the file.
|
||||
*
|
||||
* @param file the file path
|
||||
* @param version the version digest
|
||||
* @return the previously recorded vile version
|
||||
*/
|
||||
private def setVersionQuery(
|
||||
file: File,
|
||||
version: Array[Byte]
|
||||
): DBIO[Option[Array[Byte]]] = {
|
||||
val upsertQuery = FileVersions
|
||||
.insertOrUpdate(FileVersionRow(file.toString, version))
|
||||
val query = for {
|
||||
version <- getVersionQuery(file)
|
||||
_ <- upsertQuery
|
||||
} yield version
|
||||
query.transactionally
|
||||
}
|
||||
|
||||
/** The query to remove the version record.
|
||||
*
|
||||
* @param file the file path
|
||||
*/
|
||||
private def removeQuery(file: File): DBIO[Unit] = {
|
||||
val query = for {
|
||||
row <- FileVersions
|
||||
if row.path === file.toString
|
||||
} yield row
|
||||
query.delete >> DBIO.successful(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object SqlVersionsRepo {
|
||||
|
||||
/** Create the in-memory file versions repo.
|
||||
*
|
||||
* @return the versions repo backed up by SQL database
|
||||
*/
|
||||
def apply()(implicit ec: ExecutionContext): SqlVersionsRepo = {
|
||||
new SqlVersionsRepo(new SqlDatabase())
|
||||
}
|
||||
|
||||
/** Create the file versions repo.
|
||||
*
|
||||
* @param path the path to the database file
|
||||
* @return the file versions repo backed up by SQL database
|
||||
*/
|
||||
def apply(path: File)(implicit ec: ExecutionContext): SqlVersionsRepo = {
|
||||
new SqlVersionsRepo(SqlDatabase(path.toString))
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.enso.polyglot.Suggestion
|
||||
import slick.jdbc.SQLiteProfile.api._
|
||||
|
||||
import scala.annotation.nowarn
|
||||
@ -30,30 +30,47 @@ case class ArgumentRow(
|
||||
/** A row in the suggestions table.
|
||||
*
|
||||
* @param id the id of a suggestion
|
||||
* @param externalIdLeast the least significant bits of external id
|
||||
* @param externalIdMost the most significant bits of external id
|
||||
* @param kind the type of a suggestion
|
||||
* @param module the module name
|
||||
* @param name the suggestion name
|
||||
* @param selfType the self type of a suggestion
|
||||
* @param returnType the return type of a suggestion
|
||||
* @param documentation the documentation string
|
||||
* @param scopeStart the start of the scope
|
||||
* @param scopeEnd the end of the scope
|
||||
* @param scopeStartLine the line of the start position of the scope
|
||||
* @param scopeStartOffset the offset of the start position of the scope
|
||||
* @param scopeEndLine the line of the end position of the scope
|
||||
* @param scopeEndOffset the offset of the end position of the scope
|
||||
*/
|
||||
case class SuggestionRow(
|
||||
id: Option[Long],
|
||||
externalIdLeast: Option[Long],
|
||||
externalIdMost: Option[Long],
|
||||
kind: Byte,
|
||||
module: String,
|
||||
name: String,
|
||||
selfType: String,
|
||||
returnType: String,
|
||||
documentation: Option[String],
|
||||
scopeStart: Int,
|
||||
scopeEnd: Int
|
||||
scopeStartLine: Int,
|
||||
scopeStartOffset: Int,
|
||||
scopeEndLine: Int,
|
||||
scopeEndOffset: Int
|
||||
)
|
||||
|
||||
/** A row in the versions table.
|
||||
/** A row in the suggestions_version table.
|
||||
*
|
||||
* @param id the row id
|
||||
*/
|
||||
case class VersionRow(id: Option[Long])
|
||||
case class SuggestionsVersionRow(id: Option[Long])
|
||||
|
||||
/** A row in the file_versions table
|
||||
*
|
||||
* @param path the file path
|
||||
* @param digest the file version
|
||||
*/
|
||||
case class FileVersionRow(path: String, digest: Array[Byte])
|
||||
|
||||
/** The type of a suggestion. */
|
||||
object SuggestionKind {
|
||||
@ -128,51 +145,91 @@ final class ArgumentsTable(tag: Tag)
|
||||
final class SuggestionsTable(tag: Tag)
|
||||
extends Table[SuggestionRow](tag, "suggestions") {
|
||||
|
||||
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
|
||||
def kind = column[Byte]("kind")
|
||||
def name = column[String]("name")
|
||||
def selfType = column[String]("self_type")
|
||||
def returnType = column[String]("return_type")
|
||||
def documentation = column[Option[String]]("documentation")
|
||||
def scopeStart = column[Int]("scope_start", O.Default(ScopeColumn.EMPTY))
|
||||
def scopeEnd = column[Int]("scope_end", O.Default(ScopeColumn.EMPTY))
|
||||
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
|
||||
def externalIdLeast = column[Option[Long]]("external_id_least")
|
||||
def externalIdMost = column[Option[Long]]("external_id_most")
|
||||
def kind = column[Byte]("kind")
|
||||
def module = column[String]("module")
|
||||
def name = column[String]("name")
|
||||
def selfType = column[String]("self_type")
|
||||
def returnType = column[String]("return_type")
|
||||
def documentation = column[Option[String]]("documentation")
|
||||
def scopeStartLine =
|
||||
column[Int]("scope_start_line", O.Default(ScopeColumn.EMPTY))
|
||||
def scopeStartOffset =
|
||||
column[Int]("scope_start_offset", O.Default(ScopeColumn.EMPTY))
|
||||
def scopeEndLine =
|
||||
column[Int]("scope_end_line", O.Default(ScopeColumn.EMPTY))
|
||||
def scopeEndOffset =
|
||||
column[Int]("scope_end_offset", O.Default(ScopeColumn.EMPTY))
|
||||
def * =
|
||||
(
|
||||
id.?,
|
||||
externalIdLeast,
|
||||
externalIdMost,
|
||||
kind,
|
||||
module,
|
||||
name,
|
||||
selfType,
|
||||
returnType,
|
||||
documentation,
|
||||
scopeStart,
|
||||
scopeEnd
|
||||
scopeStartLine,
|
||||
scopeStartOffset,
|
||||
scopeEndLine,
|
||||
scopeEndOffset
|
||||
) <>
|
||||
(SuggestionRow.tupled, SuggestionRow.unapply)
|
||||
|
||||
def moduleIdx = index("suggestions_module_idx", module)
|
||||
def name_idx = index("suggestions_name_idx", name)
|
||||
def selfTypeIdx = index("suggestions_self_type_idx", selfType)
|
||||
def returnTypeIdx = index("suggestions_return_type_idx", returnType)
|
||||
def name_idx = index("suggestions_name_idx", name)
|
||||
def externalIdIdx =
|
||||
index("suggestions_external_id_idx", (externalIdLeast, externalIdMost))
|
||||
// NOTE: unique index should not contain nullable columns because SQLite
|
||||
// teats NULLs as distinct values.
|
||||
def uniqueIdx =
|
||||
index(
|
||||
"suggestion_unique_idx",
|
||||
(kind, name, selfType, scopeStart, scopeEnd),
|
||||
"suggestions_unique_idx",
|
||||
(
|
||||
kind,
|
||||
module,
|
||||
name,
|
||||
selfType,
|
||||
scopeStartLine,
|
||||
scopeStartOffset,
|
||||
scopeEndLine,
|
||||
scopeEndOffset
|
||||
),
|
||||
unique = true
|
||||
)
|
||||
}
|
||||
|
||||
/** The schema of the versions table. */
|
||||
/** The schema of the suggestions_version table. */
|
||||
@nowarn("msg=multiarg infix syntax")
|
||||
final class VersionsTable(tag: Tag) extends Table[VersionRow](tag, "version") {
|
||||
final class SuggestionsVersionTable(tag: Tag)
|
||||
extends Table[SuggestionsVersionRow](tag, "suggestions_version") {
|
||||
|
||||
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
|
||||
|
||||
def * = id.? <> (VersionRow.apply, VersionRow.unapply)
|
||||
def * = id.? <> (SuggestionsVersionRow.apply, SuggestionsVersionRow.unapply)
|
||||
}
|
||||
|
||||
/** The schema of the file_versions table. */
|
||||
@nowarn("msg=multiarg infix syntax")
|
||||
final class FileVersionsTable(tag: Tag)
|
||||
extends Table[FileVersionRow](tag, "file_versions") {
|
||||
|
||||
def path = column[String]("path", O.PrimaryKey)
|
||||
def digest = column[Array[Byte]]("digest")
|
||||
|
||||
def * = (path, digest) <> (FileVersionRow.tupled, FileVersionRow.unapply)
|
||||
}
|
||||
|
||||
object Arguments extends TableQuery(new ArgumentsTable(_))
|
||||
|
||||
object Suggestions extends TableQuery(new SuggestionsTable(_))
|
||||
|
||||
object Versions extends TableQuery(new VersionsTable(_))
|
||||
object SuggestionsVersions extends TableQuery(new SuggestionsVersionTable(_))
|
||||
|
||||
object FileVersions extends TableQuery(new FileVersionsTable(_))
|
||||
|
@ -0,0 +1,96 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util
|
||||
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
class FileVersionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
|
||||
|
||||
val Timeout: FiniteDuration = 5.seconds
|
||||
|
||||
val tmpdir: Path = {
|
||||
val tmp = Files.createTempDirectory("versions-repo-test")
|
||||
sys.addShutdownHook {
|
||||
Files.list(tmp).forEach { path =>
|
||||
path.toFile.delete()
|
||||
}
|
||||
tmp.toFile.delete()
|
||||
}
|
||||
tmp
|
||||
}
|
||||
|
||||
def withRepo(test: SqlVersionsRepo => Any): Any = {
|
||||
val tmpdb = Files.createTempFile(tmpdir, "versions-repo", ".db")
|
||||
val repo = SqlVersionsRepo(tmpdb.toFile)
|
||||
Await.ready(repo.init, Timeout)
|
||||
try test(repo)
|
||||
finally {
|
||||
Await.ready(repo.clean, Timeout)
|
||||
repo.close()
|
||||
}
|
||||
}
|
||||
|
||||
def nextDigest(): Array[Byte] =
|
||||
Random.nextBytes(28)
|
||||
|
||||
"FileVersionsRepo" should {
|
||||
|
||||
"insert digest" taggedAs Retry in withRepo { repo =>
|
||||
val file = new File("/foo/bar")
|
||||
val digest = nextDigest()
|
||||
val action =
|
||||
for {
|
||||
v1 <- repo.setVersion(file, digest)
|
||||
v2 <- repo.getVersion(file)
|
||||
} yield (v1, v2)
|
||||
|
||||
val (v1, v2) = Await.result(action, Timeout)
|
||||
v1 shouldBe None
|
||||
v2 shouldBe a[Some[_]]
|
||||
util.Arrays.equals(v2.get, digest) shouldBe true
|
||||
}
|
||||
|
||||
"update digest" taggedAs Retry in withRepo { repo =>
|
||||
val file = new File("/foo/bar")
|
||||
val digest1 = nextDigest()
|
||||
val digest2 = nextDigest()
|
||||
val action =
|
||||
for {
|
||||
v1 <- repo.setVersion(file, digest1)
|
||||
v2 <- repo.setVersion(file, digest2)
|
||||
v3 <- repo.getVersion(file)
|
||||
} yield (v1, v2, v3)
|
||||
|
||||
val (v1, v2, v3) = Await.result(action, Timeout)
|
||||
v1 shouldBe None
|
||||
v2 shouldBe a[Some[_]]
|
||||
v3 shouldBe a[Some[_]]
|
||||
util.Arrays.equals(v2.get, digest1) shouldBe true
|
||||
util.Arrays.equals(v3.get, digest2) shouldBe true
|
||||
}
|
||||
|
||||
"delete digest" taggedAs Retry in withRepo { repo =>
|
||||
val file = new File("/foo/bar")
|
||||
val digest = nextDigest()
|
||||
val action =
|
||||
for {
|
||||
v1 <- repo.setVersion(file, digest)
|
||||
_ <- repo.remove(file)
|
||||
v2 <- repo.getVersion(file)
|
||||
} yield (v1, v2)
|
||||
|
||||
val (v1, v2) = Await.result(action, Timeout)
|
||||
v1 shouldEqual None
|
||||
v2 shouldEqual None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +1,46 @@
|
||||
package org.enso.searcher.sql
|
||||
|
||||
import org.enso.searcher.Suggestion
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.polyglot.Suggestion
|
||||
import org.enso.testkit.RetrySpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll}
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class SuggestionsRepoTest
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with BeforeAndAfter
|
||||
with BeforeAndAfterAll
|
||||
with RetrySpec {
|
||||
class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
|
||||
|
||||
val Timeout: FiniteDuration = 10.seconds
|
||||
|
||||
val repo = SqlSuggestionsRepo()
|
||||
val tmpdir: Path = {
|
||||
val tmp = Files.createTempDirectory("suggestions-repo-test")
|
||||
sys.addShutdownHook {
|
||||
Files.list(tmp).forEach { path =>
|
||||
path.toFile.delete()
|
||||
}
|
||||
tmp.toFile.delete()
|
||||
}
|
||||
tmp
|
||||
}
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
def withRepo(test: SqlSuggestionsRepo => Any): Any = {
|
||||
val tmpdb = Files.createTempFile(tmpdir, "suggestions-repo", ".db")
|
||||
val repo = SqlSuggestionsRepo(tmpdb.toFile)
|
||||
Await.ready(repo.init, Timeout)
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
repo.close()
|
||||
}
|
||||
|
||||
before {
|
||||
Await.ready(repo.clean, Timeout)
|
||||
try test(repo)
|
||||
finally {
|
||||
Await.ready(repo.clean, Timeout)
|
||||
repo.close()
|
||||
}
|
||||
}
|
||||
|
||||
"SuggestionsRepo" should {
|
||||
|
||||
"get all suggestions" taggedAs Retry() in {
|
||||
"get all suggestions" taggedAs Retry in withRepo { repo =>
|
||||
val action =
|
||||
for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
@ -54,23 +59,27 @@ class SuggestionsRepoTest
|
||||
)
|
||||
}
|
||||
|
||||
"fail to insert duplicate suggestion" taggedAs Retry() in {
|
||||
"fail to insert duplicate suggestion" taggedAs Retry in withRepo { repo =>
|
||||
val action =
|
||||
for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
(_, ids) <- repo.insertAll(
|
||||
Seq(
|
||||
suggestion.atom,
|
||||
suggestion.atom,
|
||||
suggestion.method,
|
||||
suggestion.method,
|
||||
suggestion.function,
|
||||
suggestion.function,
|
||||
suggestion.local,
|
||||
suggestion.local
|
||||
)
|
||||
)
|
||||
all <- repo.getAll
|
||||
} yield (id1, id2, all._2)
|
||||
} yield (ids, all._2)
|
||||
|
||||
val (id1, id2, all) = Await.result(action, Timeout)
|
||||
id1 shouldBe a[Some[_]]
|
||||
id2 shouldBe a[None.type]
|
||||
val (ids, all) = Await.result(action, Timeout)
|
||||
ids(0) shouldBe a[Some[_]]
|
||||
ids(1) shouldBe a[None.type]
|
||||
all.map(_.suggestion) should contain theSameElementsAs Seq(
|
||||
suggestion.atom,
|
||||
suggestion.method,
|
||||
@ -79,22 +88,23 @@ class SuggestionsRepoTest
|
||||
)
|
||||
}
|
||||
|
||||
"fail to insertAll duplicate suggestion" taggedAs Retry() in {
|
||||
val action =
|
||||
for {
|
||||
(v1, ids) <- repo.insertAll(Seq(suggestion.local, suggestion.local))
|
||||
(v2, all) <- repo.getAll
|
||||
} yield (v1, v2, ids, all)
|
||||
"fail to insertAll duplicate suggestion" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action =
|
||||
for {
|
||||
(v1, ids) <- repo.insertAll(Seq(suggestion.local, suggestion.local))
|
||||
(v2, all) <- repo.getAll
|
||||
} yield (v1, v2, ids, all)
|
||||
|
||||
val (v1, v2, ids, all) = Await.result(action, Timeout)
|
||||
v1 shouldEqual v2
|
||||
ids.flatten.length shouldEqual 1
|
||||
all.map(_.suggestion) should contain theSameElementsAs Seq(
|
||||
suggestion.local
|
||||
)
|
||||
val (v1, v2, ids, all) = Await.result(action, Timeout)
|
||||
v1 shouldEqual v2
|
||||
ids.flatten.length shouldEqual 1
|
||||
all.map(_.suggestion) should contain theSameElementsAs Seq(
|
||||
suggestion.local
|
||||
)
|
||||
}
|
||||
|
||||
"select suggestion by id" in {
|
||||
"select suggestion by id" taggedAs Retry in withRepo { repo =>
|
||||
val action =
|
||||
for {
|
||||
Some(id) <- repo.insert(suggestion.atom)
|
||||
@ -104,7 +114,7 @@ class SuggestionsRepoTest
|
||||
Await.result(action, Timeout) shouldEqual Some(suggestion.atom)
|
||||
}
|
||||
|
||||
"remove suggestion" in {
|
||||
"remove suggestion" taggedAs Retry in withRepo { repo =>
|
||||
val action =
|
||||
for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
@ -115,13 +125,39 @@ class SuggestionsRepoTest
|
||||
id1 shouldEqual id2
|
||||
}
|
||||
|
||||
"get version" in {
|
||||
"remove suggestions by module name" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
id3 <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
(_, ids) <- repo.removeByModule(suggestion.atom.module)
|
||||
} yield (Seq(id1, id2, id3, id4).flatten, ids)
|
||||
|
||||
val (inserted, removed) = Await.result(action, Timeout)
|
||||
inserted should contain theSameElementsAs removed
|
||||
}
|
||||
|
||||
"remove all suggestions" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
(_, ids) <- repo.removeAll(Seq(suggestion.atom, suggestion.local))
|
||||
} yield (Seq(id1, id4), ids)
|
||||
|
||||
val (inserted, removed) = Await.result(action, Timeout)
|
||||
inserted should contain theSameElementsAs removed
|
||||
}
|
||||
|
||||
"get version" taggedAs Retry in withRepo { repo =>
|
||||
val action = repo.currentVersion
|
||||
|
||||
Await.result(action, Timeout) shouldEqual 0L
|
||||
}
|
||||
|
||||
"change version after insert" in {
|
||||
"change version after insert" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
@ -132,21 +168,22 @@ class SuggestionsRepoTest
|
||||
v1 should not equal v2
|
||||
}
|
||||
|
||||
"not change version after failed insert" in {
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
v2 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
v3 <- repo.currentVersion
|
||||
} yield (v1, v2, v3)
|
||||
"not change version after failed insert" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
v2 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
v3 <- repo.currentVersion
|
||||
} yield (v1, v2, v3)
|
||||
|
||||
val (v1, v2, v3) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 shouldEqual v3
|
||||
val (v1, v2, v3) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 shouldEqual v3
|
||||
}
|
||||
|
||||
"change version after remove" in {
|
||||
"change version after remove" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
@ -160,128 +197,399 @@ class SuggestionsRepoTest
|
||||
v2 should not equal v3
|
||||
}
|
||||
|
||||
"not change version after failed remove" in {
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
_ <- repo.remove(suggestion.local)
|
||||
v3 <- repo.currentVersion
|
||||
_ <- repo.remove(suggestion.local)
|
||||
v4 <- repo.currentVersion
|
||||
} yield (v1, v2, v3, v4)
|
||||
"not change version after failed remove" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
_ <- repo.remove(suggestion.local)
|
||||
v3 <- repo.currentVersion
|
||||
_ <- repo.remove(suggestion.local)
|
||||
v4 <- repo.currentVersion
|
||||
} yield (v1, v2, v3, v4)
|
||||
|
||||
val (v1, v2, v3, v4) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
v3 shouldEqual v4
|
||||
val (v1, v2, v3, v4) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
v3 shouldEqual v4
|
||||
}
|
||||
|
||||
"search suggestion by empty query" in {
|
||||
"change version after remove by module name" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
(v3, _) <- repo.removeByModule(suggestion.local.module)
|
||||
} yield (v1, v2, v3)
|
||||
|
||||
val (v1, v2, v3) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
}
|
||||
|
||||
"not change version after failed remove by module name" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
_ <- repo.removeByModule(suggestion.local.module)
|
||||
v3 <- repo.currentVersion
|
||||
(v4, _) <- repo.removeByModule(suggestion.local.module)
|
||||
} yield (v1, v2, v3, v4)
|
||||
|
||||
val (v1, v2, v3, v4) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
v3 shouldEqual v4
|
||||
}
|
||||
|
||||
"change version after remove all suggestions" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
(v3, _) <- repo.removeAll(Seq(suggestion.local))
|
||||
} yield (v1, v2, v3)
|
||||
|
||||
val (v1, v2, v3) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
}
|
||||
|
||||
"not change version after failed remove all suggestions" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
v1 <- repo.currentVersion
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v2 <- repo.currentVersion
|
||||
(v3, _) <- repo.removeAll(Seq(suggestion.local))
|
||||
(v4, _) <- repo.removeAll(Seq(suggestion.local))
|
||||
} yield (v1, v2, v3, v4)
|
||||
|
||||
val (v1, v2, v3, v4) = Await.result(action, Timeout)
|
||||
v1 should not equal v2
|
||||
v2 should not equal v3
|
||||
v3 shouldEqual v4
|
||||
}
|
||||
|
||||
"update suggestion by external id" taggedAs Retry in withRepo { repo =>
|
||||
val newReturnType = "Quux"
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
Some(id4) <- repo.insert(suggestion.local)
|
||||
res <-
|
||||
repo.updateAll(Seq(suggestion.local.externalId.get -> newReturnType))
|
||||
Some(val4) <- repo.select(id4)
|
||||
} yield (id4, res._2, val4)
|
||||
|
||||
val (suggestionId, updatedIds, result) = Await.result(action, Timeout)
|
||||
updatedIds.flatten shouldEqual Seq(suggestionId)
|
||||
result shouldEqual suggestion.local.copy(returnType = newReturnType)
|
||||
}
|
||||
|
||||
"change version after updateAll" taggedAs Retry in withRepo { repo =>
|
||||
val newReturnType = "Quux"
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
v1 <- repo.currentVersion
|
||||
res <-
|
||||
repo.updateAll(Seq(suggestion.local.externalId.get -> newReturnType))
|
||||
} yield (id4, res._2, v1, res._1)
|
||||
|
||||
val (suggestionId, updatedIds, v1, v2) = Await.result(action, Timeout)
|
||||
updatedIds shouldEqual Seq(suggestionId)
|
||||
v1 should not equal v2
|
||||
}
|
||||
|
||||
"not change version after failed updateAll" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val newReturnType = "Quux"
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
v1 <- repo.currentVersion
|
||||
res <- repo.updateAll(Seq(UUID.randomUUID() -> newReturnType))
|
||||
} yield (res._2, v1, res._1)
|
||||
|
||||
val (updatedIds, v1, v2) = Await.result(action, Timeout)
|
||||
updatedIds shouldEqual Seq(None)
|
||||
v1 shouldEqual v2
|
||||
}
|
||||
|
||||
"search suggestion by empty query" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, None, None)
|
||||
res <- repo.search(None, None, None, None, None)
|
||||
} yield res._2
|
||||
|
||||
val res = Await.result(action, Timeout)
|
||||
res.isEmpty shouldEqual true
|
||||
}
|
||||
|
||||
"search suggestion by self type" in {
|
||||
"search suggestion by module" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
id3 <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some("Test.Main"), None, None, None, None)
|
||||
} yield (id1, id2, id3, id4, res._2)
|
||||
|
||||
val (id1, id2, id3, id4, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id1, id2, id3, id4).flatten
|
||||
}
|
||||
|
||||
"search suggestion by empty module" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some(""), None, None, None, None)
|
||||
} yield res._2
|
||||
|
||||
val res = Await.result(action, Timeout)
|
||||
res.isEmpty shouldEqual true
|
||||
}
|
||||
|
||||
"search suggestion by self type" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some("Main"), None, None)
|
||||
res <- repo.search(None, Some("Main"), None, None, None)
|
||||
} yield (id2, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by return type" in {
|
||||
"search suggestion by return type" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
id3 <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, Some("MyType"), None)
|
||||
res <- repo.search(None, None, Some("MyType"), None, None)
|
||||
} yield (id3, id4, res._2)
|
||||
|
||||
val (id1, id2, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id1, id2).flatten
|
||||
}
|
||||
|
||||
"search suggestion by kind" in {
|
||||
"search suggestion by kind" taggedAs Retry in withRepo { repo =>
|
||||
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, None, Some(kinds))
|
||||
res <- repo.search(None, None, None, Some(kinds), None)
|
||||
} yield (id1, id4, res._2)
|
||||
|
||||
val (id1, id2, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id1, id2).flatten
|
||||
}
|
||||
|
||||
"search suggestion by empty kinds" in {
|
||||
"search suggestion by empty kinds" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, None, Some(Seq()))
|
||||
res <- repo.search(None, None, None, Some(Seq()), None)
|
||||
} yield res._2
|
||||
|
||||
val res = Await.result(action, Timeout)
|
||||
res.isEmpty shouldEqual true
|
||||
}
|
||||
|
||||
"search suggestion by return type and kind" in {
|
||||
"search suggestion global by scope" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <-
|
||||
repo.search(None, None, None, None, Some(Suggestion.Position(99, 42)))
|
||||
} yield (id1, id2, res._2)
|
||||
|
||||
val (id1, id2, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id1, id2).flatten
|
||||
}
|
||||
|
||||
"search suggestion local by scope" taggedAs Retry in withRepo { repo =>
|
||||
val action = for {
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
id3 <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <-
|
||||
repo.search(None, None, None, None, Some(Suggestion.Position(1, 5)))
|
||||
} yield (id1, id2, id3, res._2)
|
||||
|
||||
val (id1, id2, id3, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id1, id2, id3).flatten
|
||||
}
|
||||
|
||||
"search suggestion by module and self type" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
id2 <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some("Test.Main"), Some("Main"), None, None, None)
|
||||
} yield (id2, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by return type and kind" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, None, Some("MyType"), Some(kinds), None)
|
||||
} yield (id4, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by return type and scope" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(
|
||||
None,
|
||||
None,
|
||||
Some("MyType"),
|
||||
None,
|
||||
Some(Suggestion.Position(42, 0))
|
||||
)
|
||||
} yield (id4, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by kind and scope" taggedAs Retry in withRepo { repo =>
|
||||
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
id1 <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, Some("MyType"), Some(kinds))
|
||||
} yield (id4, res._2)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(kinds),
|
||||
Some(Suggestion.Position(99, 1))
|
||||
)
|
||||
} yield (id1, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by self and return types" in {
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some("Main"), Some("MyType"), None)
|
||||
} yield res._2
|
||||
"search suggestion by self and return types" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(None, Some("Main"), Some("MyType"), None, None)
|
||||
} yield res._2
|
||||
|
||||
val res = Await.result(action, Timeout)
|
||||
res.isEmpty shouldEqual true
|
||||
val res = Await.result(action, Timeout)
|
||||
res.isEmpty shouldEqual true
|
||||
}
|
||||
|
||||
"search suggestion by all parameters" in {
|
||||
"search suggestion by module, return type and kind" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(
|
||||
Some("Test.Main"),
|
||||
None,
|
||||
Some("MyType"),
|
||||
Some(kinds),
|
||||
None
|
||||
)
|
||||
} yield (id4, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by return type, kind and scope" taggedAs Retry in withRepo {
|
||||
repo =>
|
||||
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
id4 <- repo.insert(suggestion.local)
|
||||
res <- repo.search(
|
||||
None,
|
||||
None,
|
||||
Some("MyType"),
|
||||
Some(kinds),
|
||||
Some(Suggestion.Position(42, 0))
|
||||
)
|
||||
} yield (id4, res._2)
|
||||
|
||||
val (id, res) = Await.result(action, Timeout)
|
||||
res should contain theSameElementsAs Seq(id).flatten
|
||||
}
|
||||
|
||||
"search suggestion by all parameters" taggedAs Retry in withRepo { repo =>
|
||||
val kinds = Seq(
|
||||
Suggestion.Kind.Atom,
|
||||
Suggestion.Kind.Method,
|
||||
Suggestion.Kind.Function
|
||||
)
|
||||
val action = for {
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(Some("Main"), Some("MyType"), Some(kinds))
|
||||
_ <- repo.insert(suggestion.atom)
|
||||
_ <- repo.insert(suggestion.method)
|
||||
_ <- repo.insert(suggestion.function)
|
||||
_ <- repo.insert(suggestion.local)
|
||||
res <- repo.search(
|
||||
Some("Test.Main"),
|
||||
Some("Main"),
|
||||
Some("MyType"),
|
||||
Some(kinds),
|
||||
Some(Suggestion.Position(42, 0))
|
||||
)
|
||||
} yield res._2
|
||||
|
||||
val res = Await.result(action, Timeout)
|
||||
@ -293,7 +601,9 @@ class SuggestionsRepoTest
|
||||
|
||||
val atom: Suggestion.Atom =
|
||||
Suggestion.Atom(
|
||||
name = "Pair",
|
||||
externalId = None,
|
||||
module = "Test.Main",
|
||||
name = "Pair",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("a", "Any", false, false, None),
|
||||
Suggestion.Argument("b", "Any", false, false, None)
|
||||
@ -304,6 +614,8 @@ class SuggestionsRepoTest
|
||||
|
||||
val method: Suggestion.Method =
|
||||
Suggestion.Method(
|
||||
externalId = Some(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = "main",
|
||||
arguments = Seq(),
|
||||
selfType = "Main",
|
||||
@ -313,19 +625,27 @@ class SuggestionsRepoTest
|
||||
|
||||
val function: Suggestion.Function =
|
||||
Suggestion.Function(
|
||||
name = "bar",
|
||||
externalId = Some(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = "bar",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("x", "Number", false, true, Some("0"))
|
||||
),
|
||||
returnType = "MyType",
|
||||
scope = Suggestion.Scope(5, 9)
|
||||
scope =
|
||||
Suggestion.Scope(Suggestion.Position(1, 5), Suggestion.Position(1, 9))
|
||||
)
|
||||
|
||||
val local: Suggestion.Local =
|
||||
Suggestion.Local(
|
||||
externalId = Some(UUID.randomUUID()),
|
||||
module = "Test.Main",
|
||||
name = "bazz",
|
||||
returnType = "MyType",
|
||||
scope = Suggestion.Scope(37, 84)
|
||||
scope = Suggestion.Scope(
|
||||
Suggestion.Position(32, 0),
|
||||
Suggestion.Position(84, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,21 +9,18 @@ trait RetrySpec extends TestSuite {
|
||||
*
|
||||
* @param times the number of attempted retries
|
||||
*/
|
||||
case class Retry(times: Int) extends Tag(Retry.tagName(times)) {
|
||||
case class Retry(times: Int) extends Tag(RetryTag.name(times)) {
|
||||
assert(times > 0, "number of retries should be a positive number")
|
||||
}
|
||||
|
||||
case object Retry {
|
||||
|
||||
/** Retry the test a single time. */
|
||||
def apply(): Retry =
|
||||
new Retry(1)
|
||||
case object Retry extends Tag(RetryTag.name(1))
|
||||
|
||||
protected object RetryTag {
|
||||
val Name = "org.enso.test.retry"
|
||||
val Separator = "-"
|
||||
|
||||
/** Create the tag name. */
|
||||
def tagName(n: Int): String =
|
||||
def name(n: Int): String =
|
||||
s"$Name$Separator$n"
|
||||
|
||||
/** Parse the number of retries from the tag name. */
|
||||
@ -43,9 +40,9 @@ trait RetrySpec extends TestSuite {
|
||||
}
|
||||
} else outcomes.head
|
||||
|
||||
test.tags.find(_.contains(Retry.Name)) match {
|
||||
test.tags.find(_.contains(RetryTag.Name)) match {
|
||||
case Some(tag) =>
|
||||
go(Retry.parseRetries(tag) + 1, Nil)
|
||||
go(RetryTag.parseRetries(tag) + 1, Nil)
|
||||
case None =>
|
||||
super.withFixture(test)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import org.enso.text.editing.model.Position
|
||||
*
|
||||
* @tparam A a source type
|
||||
*/
|
||||
trait IndexedSource[A] {
|
||||
trait IndexedSource[-A] {
|
||||
|
||||
/** Converts position relative to a line to an absolute position in the
|
||||
* source.
|
||||
@ -17,6 +17,14 @@ trait IndexedSource[A] {
|
||||
* @return absolute position in the source.
|
||||
*/
|
||||
def toIndex(pos: Position, source: A): Int
|
||||
|
||||
/** Converts an absolute position to the position relative to a line.
|
||||
*
|
||||
* @param index the absolute position in the source
|
||||
* @param source the source text
|
||||
* @return the position relative to a line
|
||||
*/
|
||||
def toPosition(index: Int, source: A): Position
|
||||
}
|
||||
|
||||
object IndexedSource {
|
||||
@ -24,14 +32,57 @@ object IndexedSource {
|
||||
def apply[A](implicit is: IndexedSource[A]): IndexedSource[A] = is
|
||||
|
||||
implicit val CharSequenceIndexedSource: IndexedSource[CharSequence] =
|
||||
(pos: Position, source: CharSequence) => {
|
||||
val prefix = source.toString.linesIterator.take(pos.line)
|
||||
prefix.mkString("\n").length + pos.character
|
||||
new IndexedSource[CharSequence] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def toIndex(pos: Position, source: CharSequence): Int = {
|
||||
val prefix = source.toString.linesIterator.take(pos.line)
|
||||
val lastCarrierReturn = if (pos.line > 0) 1 else 0
|
||||
prefix.mkString("\n").length + lastCarrierReturn + pos.character
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def toPosition(index: Int, source: CharSequence): Position = {
|
||||
@scala.annotation.tailrec
|
||||
def go(lx: Int, ix: Int, lines: Iterator[String]): Position = {
|
||||
if (lines.hasNext) {
|
||||
val line = lines.next()
|
||||
if (line.length < ix)
|
||||
go(lx + 1, ix - line.length - 1, lines)
|
||||
else
|
||||
Position(lx, ix)
|
||||
} else {
|
||||
Position(lx, ix)
|
||||
}
|
||||
}
|
||||
go(0, index, source.toString.linesIterator)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val RopeIndexedSource: IndexedSource[Rope] =
|
||||
(pos: Position, source: Rope) => {
|
||||
val prefix = source.lines.take(pos.line)
|
||||
prefix.characters.length + pos.character
|
||||
new IndexedSource[Rope] {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def toIndex(pos: Position, source: Rope): Int = {
|
||||
val prefix = source.lines.take(pos.line)
|
||||
prefix.characters.length + pos.character
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
override def toPosition(index: Int, source: Rope): Position = {
|
||||
@scala.annotation.tailrec
|
||||
def go(lx: Int, ix: Int, source: Rope): Position = {
|
||||
val (hd, tl) = source.lines.splitAt(1)
|
||||
val length =
|
||||
if (tl.characters.length == 0) hd.characters.length
|
||||
else hd.characters.length - 1
|
||||
if (length < ix)
|
||||
go(lx + 1, ix - hd.characters.length, tl)
|
||||
else
|
||||
Position(lx, ix)
|
||||
}
|
||||
go(0, index, source)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
package org.enso.text.editing
|
||||
|
||||
import org.enso.text.buffer.Rope
|
||||
import org.enso.text.editing.model.Position
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
sealed abstract class IndexedSourceSpec[A: IndexedSource]
|
||||
extends AnyFlatSpec
|
||||
with Matchers {
|
||||
|
||||
val text: String =
|
||||
"""main =
|
||||
| x = 7
|
||||
| y = 42
|
||||
| IO.println(x + y)""".stripMargin.linesIterator.mkString("\n")
|
||||
|
||||
implicit def toIndexedSource(text: String): A
|
||||
|
||||
it should "convert position of the beginning of text" in {
|
||||
IndexedSource[A].toIndex(Position(0, 0), text) shouldEqual 0
|
||||
IndexedSource[A].toPosition(0, text) shouldEqual Position(0, 0)
|
||||
}
|
||||
|
||||
it should "convert a position on a first line" in {
|
||||
IndexedSource[A].toIndex(Position(0, 5), text) shouldEqual 5
|
||||
IndexedSource[A].toPosition(5, text) shouldEqual Position(0, 5)
|
||||
}
|
||||
|
||||
it should "convert last position on a first line" in {
|
||||
IndexedSource[A].toIndex(Position(0, 6), text) shouldEqual 6
|
||||
IndexedSource[A].toPosition(6, text) shouldEqual Position(0, 6)
|
||||
}
|
||||
|
||||
it should "convert first position on a line" in {
|
||||
IndexedSource[A].toIndex(Position(2, 0), text) shouldEqual 17
|
||||
IndexedSource[A].toPosition(17, text) shouldEqual Position(2, 0)
|
||||
}
|
||||
|
||||
it should "convert a position on a line" in {
|
||||
IndexedSource[A].toIndex(Position(2, 5), text) shouldEqual 22
|
||||
IndexedSource[A].toPosition(22, text) shouldEqual Position(2, 5)
|
||||
}
|
||||
|
||||
it should "convert last position on a line" in {
|
||||
IndexedSource[A].toIndex(Position(2, 10), text) shouldEqual 27
|
||||
IndexedSource[A].toPosition(27, text) shouldEqual Position(2, 10)
|
||||
}
|
||||
|
||||
it should "convert to index last position" in {
|
||||
IndexedSource[A].toIndex(Position(3, 21), text) shouldEqual 49
|
||||
IndexedSource[A].toPosition(49, text) shouldEqual Position(3, 21)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CharSequenceIndexedSourceSpec extends IndexedSourceSpec[CharSequence] {
|
||||
implicit override def toIndexedSource(text: String): CharSequence = text
|
||||
}
|
||||
|
||||
class RopeIndexedSourceSpec extends IndexedSourceSpec[Rope] {
|
||||
implicit override def toIndexedSource(text: String): Rope = Rope(text)
|
||||
}
|
Loading…
Reference in New Issue
Block a user