diff --git a/modules/ast/src/main/scala/playground/smithyql/AST.scala b/modules/ast/src/main/scala/playground/smithyql/AST.scala index a542a1c7..dc5879e5 100644 --- a/modules/ast/src/main/scala/playground/smithyql/AST.scala +++ b/modules/ast/src/main/scala/playground/smithyql/AST.scala @@ -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[_]]( diff --git a/modules/examples/src/main/smithy/demo.smithy b/modules/examples/src/main/smithy/demo.smithy index 80f04d05..250ebdde 100644 --- a/modules/examples/src/main/smithy/demo.smithy +++ b/modules/examples/src/main/smithy/demo.smithy @@ -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 diff --git a/modules/formatter/src/main/scala/playground/smithyql/format/Formatter.scala b/modules/formatter/src/main/scala/playground/smithyql/format/Formatter.scala index 0cf1ae1f..ad25cc25 100644 --- a/modules/formatter/src/main/scala/playground/smithyql/format/Formatter.scala +++ b/modules/formatter/src/main/scala/playground/smithyql/format/Formatter.scala @@ -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 @@ -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 @@ -177,6 +189,16 @@ 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]] )( @@ -184,12 +206,7 @@ private[format] object FormattingVisitor extends ASTVisitor[WithSource, Doc] { v 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 diff --git a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala index 58c99f4e..3fe42035 100644 --- a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala +++ b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala @@ -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 { @@ -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 } diff --git a/modules/language-support/src/main/scala/playground/language/CompletionVisitor.scala b/modules/language-support/src/main/scala/playground/language/CompletionVisitor.scala index f8421a93..87af2edc 100644 --- a/modules/language-support/src/main/scala/playground/language/CompletionVisitor.scala +++ b/modules/language-support/src/main/scala/playground/language/CompletionVisitor.scala @@ -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 @@ -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 @@ -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, @@ -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 = { @@ -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)