Skip to content
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

@examples trait for completions #339

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions modules/ast/src/main/scala/playground/smithyql/AST.scala
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ sealed trait InputNode[F[_]] extends AST[F] {
fk: F ~> G
): InputNode[G]

def asStruct: Option[Struct[F]] =
this match {
case Struct(fields) => Some(Struct(fields))
case _ => None
}

}

final case class OperationName[F[_]](
Expand Down
22 changes: 22 additions & 0 deletions modules/examples/src/main/smithy/demo.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ operation GetVersion {
@documentation("""
Create a hero.
""")
@examples([{
title: "Valid input"
documentation: "This is a valid input"
input: {
hero: {
good: {
howGood: 10
}
}
}
}, {
title: "Valid input v2"
documentation: "This is also a valid input, but for a bad hero"
input: {
hero: {
bad: {
evilName: "Evil"
powerLevel: 10
}
}
}
}])
operation CreateHero {
input: CreateHeroInput
output: CreateHeroOutput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ object Formatter {
implicit val useClauseFormatter: Formatter[UseClause] = writeDoc
implicit val preludeFormatter: Formatter[Prelude] = writeDoc
implicit val qonFormatter: Formatter[QueryOperationName] = writeDoc

implicit val fieldsFormatter: Formatter[Struct.Fields] =
(
f,
w,
) => FormattingVisitor.writeStructFields(WithSource.liftId(f)).renderTrim(w)

implicit val inputNodeFormatter: Formatter[InputNode] = writeDoc
implicit val structFormatter: Formatter[Struct] = writeDoc
implicit val listedFormatter: Formatter[Listed] = writeDoc
Expand Down Expand Up @@ -108,9 +115,14 @@ private[format] object FormattingVisitor extends ASTVisitor[WithSource, Doc] { v
query: WithSource[Query[WithSource]]
): Doc = printGeneric(query)

// no braces
def writeStructFields(
fields: WithSource[Struct.Fields[WithSource]]
): Doc = writeCommaSeparated(fields.map(_.value))(writeField)

override def struct(
fields: WithSource[Struct.Fields[WithSource]]
): Doc = writeBracketed(fields.map(_.value))(Doc.char('{'), Doc.char('}'))(writeField)
): Doc = Doc.char('{') + writeStructFields(fields) + Doc.char('}')

private def forceLineAfterTrailingComments[A](
printer: WithSource[A] => Doc
Expand Down Expand Up @@ -177,19 +189,24 @@ private[format] object FormattingVisitor extends ASTVisitor[WithSource, Doc] { v
// Force newlines between fields
fields.map(renderField).intercalate(Doc.hardLine)

private def writeCommaSeparated[T](
items: WithSource[List[T]]
)(
renderItem: T => Doc
): Doc =
Doc.hardLine +
printWithComments(items)(writeFields(_)(renderItem(_) + Doc.comma))
.indent(2) +
Doc.hardLine

private def writeBracketed[T](
items: WithSource[List[T]]
)(
before: Doc,
after: Doc,
)(
renderItem: T => Doc
): Doc =
before + Doc.hardLine +
printWithComments(items)(writeFields(_)(renderItem(_) + Doc.comma))
.indent(2) +
Doc.hardLine +
after
): Doc = before + writeCommaSeparated(items)(renderItem) + after

def writeIdent(
ident: QualifiedIdentifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import playground.smithyql.SourceFile
import playground.smithyql.WithSource
import playground.smithyql.parser.SourceParser
import playground.smithyql.syntax.*
import smithy.api.Examples
import smithy4s.Hints
import smithy4s.dynamic.DynamicSchemaIndex

trait CompletionProvider {
Expand Down Expand Up @@ -105,7 +107,12 @@ object CompletionProvider {
.service
.endpoints
.map { endpoint =>
OperationName[Id](endpoint.name) -> endpoint.input.compile(CompletionVisitor)
OperationName[Id](endpoint.name) -> endpoint
.input
.addHints(
endpoint.hints.get(Examples).map(Hints(_)).getOrElse(Hints.empty)
)
.compile(CompletionVisitor)
}
.toMap
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package playground.language
import cats.Id
import cats.kernel.Eq
import cats.syntax.all.*
import playground.NodeEncoder
import playground.ServiceNameExtractor
import playground.TextUtils
import playground.language.CompletionItem.InsertUseClause.NotRequired
Expand All @@ -23,6 +24,7 @@ import playground.smithyql.WithSource
import playground.smithyql.format.Formatter
import smithy.api
import smithy4s.Bijection
import smithy4s.Document
import smithy4s.Endpoint
import smithy4s.Hints
import smithy4s.Lazy
Expand Down Expand Up @@ -157,15 +159,17 @@ object CompletionItem {
label: String,
insertText: InsertText,
schema: Schema[?],
sortTextOverride: Option[String] = None,
): CompletionItem = {
val isField = kind === CompletionItemKind.Field

val sortText =
val sortText = sortTextOverride.orElse {
isField match {
case true if isRequiredField(schema) => Some(s"1_$label")
case true => Some(s"2_$label")
case false => None
}
}

CompletionItem(
kind = kind,
Expand Down Expand Up @@ -383,6 +387,73 @@ object CompletionItem {
)
}

// Examples for operation inputs.
// TODO: currently only works inside the struct (and assumes that by rendering only fields, no braces).
// If/when we ever have graceful parsing in completions, we should handle other contexts, such as being outside of the struct.
def forInputExamples[S](
schema: Schema[S]
): List[CompletionItem] = {
val documentDecoder = Document.Decoder.fromSchema(schema)
val nodeEncoder = NodeEncoder.derive(schema)

case class Sample(
name: String,
documentation: Option[String],
inputObject: Struct[Id],
)

def decodeSample(
example: api.Example
): Option[Sample] =
for {
input <- example.input
decoded <- documentDecoder.decode(input).toOption
// note: we could've transcoded from Document to Node directly, without the intermediate decoding
// but the examples we suggest should be valid, and this is the only way to ensure that.
encoded = nodeEncoder.toNode(decoded)

// we're only covering inputs, and operation inputs must be structures.
asObject <- encoded.asStruct
} yield Sample(
name = example.title,
documentation = example.documentation,
inputObject = asObject,
)

def completionForSample(
sample: Sample,
index: Int,
): CompletionItem = {
val text = Formatter[Struct.Fields]
.format(
sample
.inputObject
.fields
.mapK(WithSource.liftId),
Int.MaxValue,
)

CompletionItem.fromHints(
kind = CompletionItemKind.Constant /* todo */,
label = s"Example: ${sample.name}",
insertText = InsertText.JustString(text),
// issue: this doesn't work if the schema already has a Documentation hint. We should remove it first, or do something else.
schema = schema.addHints(
sample.documentation.map(api.Documentation(_)).map(Hints(_)).getOrElse(Hints.empty)
),
sortTextOverride = Some(s"0_$index"),
)
}

schema
.hints
.get(api.Examples)
.foldMap(_.value)
.flatMap(decodeSample)
.zipWithIndex
.map(completionForSample.tupled)
}

def deprecationString(
info: api.Deprecated
): String = {
Expand Down Expand Up @@ -570,11 +641,16 @@ object CompletionVisitor extends SchemaVisitor[CompletionResolver] {
fields: Vector[Field[S, ?]],
make: IndexedSeq[Any] => S,
): CompletionResolver[S] = {
// Artificial schema resembling this one. Should be pretty much equivalent.
val schema = Schema.struct(fields)(make).addHints(hints).withId(shapeId)

val compiledFields = fields.map(field => (field, field.schema.compile(this)))

val examples = CompletionItem.forInputExamples(schema)

structLike(
inBody =
fields
examples ++ fields
// todo: filter out present fields
.sortBy(field => (field.isRequired && !field.hasDefaultValue, field.label))
.map(CompletionItem.fromField)
Expand Down
Loading