-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(language-server): Pass token types to LSP [LNG-285] #999
Conversation
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Make nil option of bottom * Fix tests * Make literals of data type * Add unit tests * Remove print
#979) * Add noEmptyResponse * Fix tests
Use js.UndefOr
* chore(main): release aqua 0.13.0 * chore: Bump aqua version to 0.13.0
…977) * Refactor * Refactor type system * Remove println * Fix renaming * Add unit tests * Do not convert to call arrow * Check ability * Refactor captured values resolution * Remove println * Fix fields gathering * Remove println * Remove println * Fix renaming, export ability * Rename only abilities * Fix unit tests * Fix captured arrows renaming * Add comments * Refactor * Rename only arrows * Add comments, refactor * Add comments * Rename method * Add integration test --------- Co-authored-by: Anatolios Laskaris <[email protected]> Co-authored-by: Dima <[email protected]>
Fix e2e
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Use main branch
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Dima <[email protected]>
# Conflicts: # semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala
…-lsp # Conflicts: # aqua-src/antithesis.aqua # language-server/language-server-api/.js/src/main/scala/aqua/lsp/AquaLSP.scala # model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala # model/src/main/scala/aqua/model/ArgsCall.scala # model/src/main/scala/aqua/model/ValueModel.scala # types/src/main/scala/aqua/types/Type.scala
WarningInfo(start, end, message, fileSpan.name) | ||
} | ||
} | ||
|
||
@JSExportTopLevel("AquaLSP") | ||
object AquaLSP extends App with Logging { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is inheritance from App
needed?
val unquoted = str.substring(1, str.length - 1) | ||
TokenLocation.fromSpan(span).map(l => TokenImport(l, unquoted)) | ||
}.toJSArray | ||
|
||
val result = fileRes match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just return this expression instead of creating a val
?
} yield { | ||
TokenLocation(span.name, startLC._1, startLC._2, endLC._1, endLC._2) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} yield { | |
TokenLocation(span.name, startLC._1, startLC._2, endLC._1, endLC._2) | |
} | |
} yield TokenLocation(span.name, startLC._1, startLC._2, endLC._1, endLC._2) |
private def tokensToJs(tokens: List[TokenInfo[FileSpan.F]]): js.Array[TokenInfoJs] = { | ||
tokens.flatMap { ti => | ||
TokenLocation.fromSpan(ti.token.unit._1).map { tl => | ||
val typeName = ti.`type`.show | ||
TokenInfoJs(tl, typeName) | ||
} | ||
}.toJSArray | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private def tokensToJs(tokens: List[TokenInfo[FileSpan.F]]): js.Array[TokenInfoJs] = { | |
tokens.flatMap { ti => | |
TokenLocation.fromSpan(ti.token.unit._1).map { tl => | |
val typeName = ti.`type`.show | |
TokenInfoJs(tl, typeName) | |
} | |
}.toJSArray | |
} | |
private def tokensToJs(tokens: List[TokenInfo[FileSpan.F]]): js.Array[TokenInfoJs] = | |
tokens.flatMap { ti => | |
TokenLocation.fromSpan(ti.token.unit._1).map { tl => | |
val typeName = ti.`type`.show | |
TokenInfoJs(tl, typeName) | |
} | |
}.toJSArray |
def lspToCompilationResult(lsp: LspContext[FileSpan.F]): CompilationResult = { | ||
val errors = lsp.errors.map(CompileError.apply).flatMap(errorToInfo) | ||
val warnings = lsp.warnings.map(CompileWarning.apply).flatMap(warningToInfo) | ||
errors match |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
errors match | |
errors match |
): State[X, Unit] = { | ||
val allTokens = | ||
definition +: fields.map { fieldDef => | ||
fieldDef.copy(name = combineFieldName(definition.name, fieldDef.name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AbilityType.fullName
?
importTokens: List[LiteralToken[S]] = Nil, | ||
errors: List[SemanticError[S]] = Nil, | ||
warnings: List[SemanticWarning[S]] = Nil | ||
) | ||
) { | ||
lazy val allLocations: List[(Token[S], Token[S])] = variables.flatMap(_.allLocations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like types like (Token[S], Token[S])
. No one will remember how those two tokens are connected in two weeks.
@@ -90,7 +92,9 @@ object LspContext { | |||
val prefix = name + "." | |||
ctx.copy( | |||
raw = ctx.raw.setAbility(name, ctxAb.raw), | |||
tokens = ctx.tokens ++ ctxAb.tokens.map(kv => (prefix + kv._1) -> kv._2) | |||
variables = ctx.variables ++ ctxAb.variables.map(v => | |||
v.copy(definition = v.definition.copy(name = prefix + v.definition.name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This is one more place where knowledge of how to construct compound name (through dot) is stored. How about calling some common source of truth like AbilityType.fullName
?
/*println(tokens.filter(v => v._1 == checkName && v._2.`type` == `type`).map { | ||
case (name, expr) => | ||
val span = expr.token.unit._1 | ||
println(s"$name(${span.startIndex}:${span.endIndex}) ${expr.`type`}") | ||
})*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove it?
println(res.allLocations.map { case (l, r) => | ||
val lSpan = l.unit._1 | ||
val rSpan = r.unit._1 | ||
s"($l($lSpan):$r($rSpan))" | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove println
?
res.checkLocations(120, 121, 224, 225) shouldBe true | ||
|
||
res.checkTokenLoc("a", 152, 153, LiteralType.string) shouldBe true | ||
res.checkLocations(152, 153, 172, 173) shouldBe true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those tests will be hard to modify, maintain and debug. I hope we will not modify them a lot, but still why don't you write a function that finds all occurrences of a name inside source by regex? It is much easier to comprehend "Nth occurrence of a
" then 152, 153
_ <- locations | ||
.addTokenWithFields(token.value, token, names.toList) | ||
.whenA(defs.nonEmpty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove it? Also now token
argument is unused
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic was moved to AbilityInterpreter
case class DefinitionInfo[S[_]](name: String, token: Token[S], `type`: Type) | ||
case class VariableInfo[S[_]](definition: DefinitionInfo[S], occurrences: List[Token[S]] = Nil) { | ||
def allLocations: List[(Token[S], Token[S])] = occurrences.map(_ -> definition.token) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Put it into package object?
def addDefinitions(newDefinitions: List[DefinitionInfo[S]]): LocationsState[S] = { | ||
copy(variables = newDefinitions.map(d => VariableInfo(d)) ++ variables) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def addDefinitions(newDefinitions: List[DefinitionInfo[S]]): LocationsState[S] = { | |
copy(variables = newDefinitions.map(d => VariableInfo(d)) ++ variables) | |
} | |
def addDefinitions(newDefinitions: List[DefinitionInfo[S]]): LocationsState[S] = | |
copy(variables = newDefinitions.map(d => VariableInfo(d)) ++ variables) | |
token: Token[S] | ||
): List[VariableInfo[S]] = vars match { | ||
case Nil => | ||
logger.error(s"Unexpected. Cannot add occurrence for $name") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not internalError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it shouldn't stop compilation
case head :: tail => | ||
if (head.definition.name == name) | ||
head.copy(occurrences = token +: head.occurrences) :: tail | ||
else | ||
head :: addOccurrenceToFirst(tail, name, token) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can extract general method to helpers
package:
extension [A](l: List[A]) {
def updatedFirst[B >: A](p: A => Boolean, f: A => B): List[B] = {
def update(left: List[B], right: List[A]): List[B] =
right match {
case a :: tail if p(a) => left.reverse ::: f(a) :: tail
case a :: tail => update(a :: left, tail)
case Nil => left.reverse
}
update(Nil, l)
}
}
): LocationsState[S] = { | ||
copy(variables = addOccurrenceToFirst(variables, name, token)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
): LocationsState[S] = { | |
copy(variables = addOccurrenceToFirst(variables, name, token)) | |
} | |
): LocationsState[S] = | |
copy(variables = addOccurrenceToFirst(variables, name, token)) | |
locations: List[(String, Token[S])] | ||
): LocationsState[S] = | ||
locations.foldLeft(this) { case (st, (name, token)) => | ||
st.copy(variables = addOccurrenceToFirst(variables, name, token)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I understand right that variables
referenced here are variables
of outer state? So we add only the last location here because state is actually not saved. You can use addLocation
here instead of repeating it and issue will be fixed
@@ -16,38 +17,73 @@ import org.scalatest.matchers.should.Matchers | |||
|
|||
class AquaLSPSpec extends AnyFlatSpec with Matchers with Inside { | |||
|
|||
private def getByPosition(code: String, str: String, position: Int): Option[(Int, Int)] = { | |||
str.r.findAllMatchIn(code).toList.lift(position).map(r => (r.start, r.end)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: You can wrap str
with word boundary regex so that var
is not matched in somevariable
.
Now it is unnecessary, maybe just for future tests
@@ -79,49 +115,75 @@ class AquaLSPSpec extends AnyFlatSpec with Matchers with Inside { | |||
id => txt => Parser.parse(Parser.parserSchema)(txt), | |||
AquaCompilerConf(ConstantRaw.defaultConstants(None)) | |||
) | |||
.leftMap { errors => | |||
println(errors) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is println
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
} | ||
|
||
if (printFiltered) | ||
println( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this print needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
|
||
object list { | ||
extension[A] (l: List[A]) { | ||
def updateFirst[B >: A](p: A => Boolean, f: A => B): List[B] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: leave a comment
Description
Pass token types to handle this info in language-server (show types on hover in VSCode)
Implementation Details
Gather all tokens with types in
LocationsState
and pass it toCompilationResult
in lsp packageChecklist
Reviewer Checklist