Fix the search/getSuggestionsDatabase API (#1021)

This commit is contained in:
Dmitry Bushev 2020-07-21 23:15:14 +03:00 committed by GitHub
parent 27a322db26
commit ded61865a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 516 additions and 103 deletions

View File

@ -3,11 +3,12 @@ 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.{
SuggestionDatabaseEntry,
SuggestionId,
SuggestionKind,
SuggestionsDatabaseUpdate
}
import org.enso.searcher.SuggestionEntry
import org.enso.text.editing.model.Position
/**
@ -34,7 +35,7 @@ object SearchApi {
extends Method("search/getSuggestionsDatabase") {
case class Result(
entries: Seq[SuggestionEntry],
entries: Seq[SuggestionDatabaseEntry],
currentVersion: Long
)

View File

@ -13,6 +13,79 @@ object SearchProtocol {
type SuggestionId = Long
private object CodecField {
val Type = "type"
}
private object CodecType {
val Add = "Add"
val Remove = "Remove"
val Modify = "Modify"
}
private object SuggestionType {
val Atom = "atom"
val Method = "method"
val Function = "function"
val Local = "local"
}
implicit val suggestionEncoder: Encoder[Suggestion] =
Encoder.instance[Suggestion] {
case atom: Suggestion.Atom =>
Encoder[Suggestion.Atom]
.apply(atom)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Atom.asJson))
.dropNullValues
case method: Suggestion.Method =>
Encoder[Suggestion.Method]
.apply(method)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Method.asJson)
)
.dropNullValues
case function: Suggestion.Function =>
Encoder[Suggestion.Function]
.apply(function)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Function.asJson)
)
.dropNullValues
case local: Suggestion.Local =>
Encoder[Suggestion.Local]
.apply(local)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Local.asJson))
.dropNullValues
}
implicit val suggestionDecoder: Decoder[Suggestion] =
Decoder.instance { cursor =>
cursor.downField(CodecField.Type).as[String].flatMap {
case SuggestionType.Atom =>
Decoder[Suggestion.Atom].tryDecode(cursor)
case SuggestionType.Method =>
Decoder[Suggestion.Method].tryDecode(cursor)
case SuggestionType.Function =>
Decoder[Suggestion.Function].tryDecode(cursor)
case SuggestionType.Local =>
Decoder[Suggestion.Local].tryDecode(cursor)
}
}
sealed trait SuggestionsDatabaseUpdate
object SuggestionsDatabaseUpdate {
@ -38,20 +111,6 @@ object SearchProtocol {
case class Modify(id: SuggestionId, returnType: String)
extends SuggestionsDatabaseUpdate
private object CodecField {
val Type = "type"
}
private object CodecType {
val Add = "Add"
val Remove = "Remove"
val Modify = "Modify"
}
implicit val decoder: Decoder[SuggestionsDatabaseUpdate] =
Decoder.instance { cursor =>
cursor.downField(CodecField.Type).as[String].flatMap {
@ -85,65 +144,6 @@ object SearchProtocol {
.deepMerge(Json.obj(CodecField.Type -> CodecType.Modify.asJson))
.dropNullValues
}
private object SuggestionType {
val Atom = "atom"
val Method = "method"
val Function = "function"
val Local = "local"
}
implicit val suggestionEncoder: Encoder[Suggestion] =
Encoder.instance[Suggestion] {
case atom: Suggestion.Atom =>
Encoder[Suggestion.Atom]
.apply(atom)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Atom.asJson))
.dropNullValues
case method: Suggestion.Method =>
Encoder[Suggestion.Method]
.apply(method)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Method.asJson)
)
.dropNullValues
case function: Suggestion.Function =>
Encoder[Suggestion.Function]
.apply(function)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Function.asJson)
)
.dropNullValues
case local: Suggestion.Local =>
Encoder[Suggestion.Local]
.apply(local)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Local.asJson))
.dropNullValues
}
implicit val suggestionDecoder: Decoder[Suggestion] =
Decoder.instance { cursor =>
cursor.downField(CodecField.Type).as[String].flatMap {
case SuggestionType.Atom =>
Decoder[Suggestion.Atom].tryDecode(cursor)
case SuggestionType.Method =>
Decoder[Suggestion.Method].tryDecode(cursor)
case SuggestionType.Function =>
Decoder[Suggestion.Function].tryDecode(cursor)
case SuggestionType.Local =>
Decoder[Suggestion.Local].tryDecode(cursor)
}
}
}
/** The type of a suggestion. */
@ -193,6 +193,49 @@ object SearchProtocol {
}
}
/**
* The entry in the suggestions database.
*
* @param id the suggestion id
* @param suggestion the suggestion
*/
case class SuggestionDatabaseEntry(id: SuggestionId, suggestion: Suggestion)
object SuggestionDatabaseEntry {
/**
* Create the database entry from the polyglot suggestion entry.
*
* @param entry the suggestion entry
* @return the database entry
*/
def apply(entry: SuggestionEntry): SuggestionDatabaseEntry =
new SuggestionDatabaseEntry(entry.id, entry.suggestion)
private object CodecField {
val Id = "id"
val Suggestion = "suggestion"
}
implicit val encoder: Encoder[SuggestionDatabaseEntry] =
Encoder.instance { entry =>
Json.obj(
CodecField.Id -> entry.id.asJson,
CodecField.Suggestion -> Encoder[Suggestion].apply(entry.suggestion)
)
}
implicit val decoder: Decoder[SuggestionDatabaseEntry] =
Decoder.instance { cursor =>
for {
id <- cursor.downField(CodecField.Id).as[SuggestionId]
suggestion <- cursor.downField(CodecField.Suggestion).as[Suggestion]
} yield SuggestionDatabaseEntry(id, suggestion)
}
}
/** A notification about changes in the suggestions database.
*
* @param currentVersion current version of the suggestions database
@ -213,7 +256,7 @@ object SearchProtocol {
*/
case class GetSuggestionsDatabaseResult(
currentVersion: Long,
entries: Seq[SuggestionEntry]
entries: Seq[SuggestionDatabaseEntry]
)
/** The request to receive the current version of the suggestions database. */

View File

@ -173,7 +173,13 @@ final class SuggestionsHandler(
case GetSuggestionsDatabase =>
repo.getAll
.map(GetSuggestionsDatabaseResult.tupled)
.map {
case (version, entries) =>
GetSuggestionsDatabaseResult(
version,
entries.map(SuggestionDatabaseEntry(_))
)
}
.pipeTo(sender())
case Completion(path, pos, selfType, returnType, tags) =>

View File

@ -11,24 +11,17 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
AcquireCapability,
CapabilityAcquired
}
import org.enso.languageserver.data.{
CapabilityRegistration,
Config,
DirectoriesConfig,
ExecutionContextConfig,
FileManagerConfig,
PathWatcherConfig,
ReceivesSuggestionsDatabaseUpdates
}
import org.enso.languageserver.data._
import org.enso.languageserver.filemanager.Path
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
import org.enso.languageserver.runtime.SearchProtocol.SuggestionDatabaseEntry
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.SuggestionsRepo
import org.enso.searcher.sql.SqlSuggestionsRepo
import org.enso.text.editing.model.Position
import org.enso.testkit.RetrySpec
import org.enso.text.editing.model.Position
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
@ -162,7 +155,7 @@ class SuggestionsHandlerSpec
expectMsg(
SearchProtocol.GetSuggestionsDatabaseResult(
1,
Seq(SuggestionEntry(1L, Suggestions.atom))
Seq(SuggestionDatabaseEntry(1L, Suggestions.atom))
)
)
}

View File

@ -178,6 +178,107 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
}
""")
// get suggestions database
client.send(json.getSuggestionsDatabase(0))
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"id" : 0,
"result" : {
"entries" : [
{
"id" : 3,
"suggestion" : {
"type" : "function",
"externalId" : ${Suggestions.function.externalId.get},
"module" : "Test.Main",
"name" : "print",
"arguments" : [
],
"returnType" : "IO",
"scope" : {
"start" : {
"line" : 1,
"character" : 9
},
"end" : {
"line" : 1,
"character" : 22
}
}
}
},
{
"id" : 1,
"suggestion" : {
"type" : "atom",
"module" : "Test.Main",
"name" : "MyType",
"arguments" : [
{
"name" : "a",
"reprType" : "Any",
"isSuspended" : false,
"hasDefault" : false,
"defaultValue" : null
}
],
"returnType" : "MyAtom"
}
},
{
"id" : 2,
"suggestion" : {
"type" : "method",
"externalId" : ${Suggestions.method.externalId.get},
"module" : "Test.Main",
"name" : "foo",
"arguments" : [
{
"name" : "this",
"reprType" : "MyType",
"isSuspended" : false,
"hasDefault" : false,
"defaultValue" : null
},
{
"name" : "foo",
"reprType" : "Number",
"isSuspended" : false,
"hasDefault" : true,
"defaultValue" : "42"
}
],
"selfType" : "MyType",
"returnType" : "Number",
"documentation" : "Lovely"
}
},
{
"id" : 4,
"suggestion" : {
"type" : "local",
"externalId" : ${Suggestions.local.externalId.get},
"module" : "Test.Main",
"name" : "x",
"returnType" : "Number",
"scope" : {
"start" : {
"line" : 21,
"character" : 0
},
"end" : {
"line" : 89,
"character" : 0
}
}
}
}
],
"currentVersion" : 4
}
}
""")
// remove items
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(

View File

@ -105,20 +105,21 @@ class RuntimeServerTest
val idFooY = metadata.addItem(81, 8)
val idFooZ = metadata.addItem(98, 5)
val code = metadata.appendToCode(
"""
|main =
| x = 6
| y = x.foo 5
| z = y + 5
| z
|
|Number.foo = x ->
| y = this + 3
| z = y * x
| z
|""".stripMargin
)
def code =
metadata.appendToCode(
"""
|main =
| x = 6
| y = x.foo 5
| z = y + 5
| z
|
|Number.foo = x ->
| y = this + 3
| z = y * x
| z
|""".stripMargin.linesIterator.mkString("\n")
)
object Update {
@ -459,6 +460,274 @@ class RuntimeServerTest
context.consumeOut shouldEqual List()
}
it should "support file modification operations with attached ids" in {
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val metadata = new Metadata
val idMain = metadata.addItem(7, 2)
val code = metadata.appendToCode("main = 84")
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, code)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
fooFile,
code,
false
)
)
)
context.receive shouldEqual None
// 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.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(idMain, Some("Number"), Some("84"), None)
)
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Foo",
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
"Test.Foo",
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
)
)
)
)
)
// Modify the file
context.send(
Api.Request(
Api.EditFileNotification(
fooFile,
Seq(
TextEdit(
model.Range(model.Position(0, 0), model.Position(0, 9)),
"main = 42"
)
)
)
)
)
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(idMain, Some("Number"), Some("42"), None)
)
)
)
)
}
it should "send suggestion notifications when file executed" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val idMain = context.Main.metadata.addItem(7, 47)
val idMainUpdate =
Api.Response(
Api.ExpressionValuesComputed(
contextId,
Vector(
Api.ExpressionValueUpdate(
idMain,
Some("Number"),
Some("50"),
None
)
)
)
)
val mainFile = context.writeMain(context.Main.code)
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
mainFile,
context.Main.code,
false
)
)
)
context.receive shouldEqual None
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer(mainFile, "Main", "main"),
None,
Vector()
)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item1))
)
context.receive(7) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
context.Main.Update.mainX(contextId),
context.Main.Update.mainY(contextId),
context.Main.Update.mainZ(contextId),
idMainUpdate,
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Main",
List(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
"Test.Main",
"main",
List(Suggestion.Argument("this", "Any", false, false, None)),
"here",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Main",
"foo",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Number",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainX),
"Test.Main",
"x",
"Any",
Suggestion
.Scope(Suggestion.Position(1, 6), Suggestion.Position(6, 0))
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainY),
"Test.Main",
"y",
"Any",
Suggestion
.Scope(Suggestion.Position(1, 6), Suggestion.Position(6, 0))
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainZ),
"Test.Main",
"z",
"Any",
Suggestion
.Scope(Suggestion.Position(1, 6), Suggestion.Position(6, 0))
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idFooY),
"Test.Main",
"y",
"Any",
Suggestion
.Scope(Suggestion.Position(7, 17), Suggestion.Position(10, 5))
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idFooZ),
"Test.Main",
"z",
"Any",
Suggestion
.Scope(Suggestion.Position(7, 17), Suggestion.Position(10, 5))
)
)
)
)
)
)
// push foo call
val item2 = Api.StackItem.LocalCall(context.Main.idMainY)
context.send(
Api.Request(requestId, Api.PushContextRequest(contextId, item2))
)
context.receive(4) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
context.Main.Update.fooY(contextId),
context.Main.Update.fooZ(contextId)
)
// pop foo call
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PopContextResponse(contextId)),
idMainUpdate
)
// pop main
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PopContextResponse(contextId))
)
// pop empty stack
context.send(Api.Request(requestId, Api.PopContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.EmptyStackError(contextId))
)
}
it should "send suggestion notifications when file modified" in {
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()