Integration with the Searcher Database (#994)

This commit is contained in:
Dmitry Bushev 2020-07-20 11:00:49 +03:00 committed by GitHub
parent 2801f58ba9
commit 30d136a141
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 3387 additions and 1059 deletions

View File

@ -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(

View File

@ -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"
}
```

View 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.

View File

@ -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()

View File

@ -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()
}
}

View File

@ -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
)
)

View File

@ -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] =

View 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]
}

View File

@ -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"))
}
}

View File

@ -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

View File

@ -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]].

View File

@ -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) {

View File

@ -185,8 +185,8 @@ class JsonConnectionController(
)
case SearchProtocol.SuggestionsDatabaseUpdateNotification(
updates,
version
version,
updates
) =>
webActor ! Notification(
SearchApi.SuggestionsDatabaseUpdates,

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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(".")
}

View File

@ -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")
}

View 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
}
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -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 = _

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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"""

View File

@ -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",

View File

@ -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
}
}
}
}

View File

@ -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"
}
}
""")
}
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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.
*

View File

@ -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);
});
}
/**

View File

@ -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
}
/**

View File

@ -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 {

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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))

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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();

View File

@ -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 =

View File

@ -3,5 +3,6 @@ searcher {
url = "jdbc:sqlite::memory:"
driver = "org.sqlite.JDBC"
connectionPool = "HikariCP"
properties.journal_mode = "wal"
}
}

View File

@ -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]
}

View File

@ -1,5 +1,7 @@
package org.enso.searcher
import org.enso.polyglot.Suggestion
/** The entry in the suggestions database.
*
* @param id the suggestion id

View File

@ -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]])]
}

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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(_))

View File

@ -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
}
}
}

View File

@ -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)
)
)
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}