diff --git a/modules/ast/src/main/scala/playground/smithyql/AST.scala b/modules/ast/src/main/scala/playground/smithyql/AST.scala index a542a1c7..1895de91 100644 --- a/modules/ast/src/main/scala/playground/smithyql/AST.scala +++ b/modules/ast/src/main/scala/playground/smithyql/AST.scala @@ -4,6 +4,7 @@ import cats.Applicative import cats.Functor import cats.Id import cats.Show +import cats.arrow.FunctionK import cats.data.NonEmptyList import cats.kernel.Eq import cats.kernel.Order @@ -87,7 +88,7 @@ final case class SourceFile[F[_]]( def mapK[G[_]: Functor]( fk: F ~> G - ): AST[G] = SourceFile( + ): SourceFile[G] = SourceFile( prelude = prelude.mapK(fk), statements = fk(statements).map(_.map(_.mapK(fk))), ) @@ -132,9 +133,9 @@ final case class OperationName[F[_]]( text: String ) extends AST[F] { - def mapK[G[_]: Functor]( - fk: F ~> G - ): OperationName[G] = copy() + def mapK[G[_]: Functor](fk: FunctionK[F, G]): OperationName[G] = retag[G] + + def retag[G[_]]: OperationName[G] = copy() } @@ -189,7 +190,7 @@ final case class QueryOperationName[F[_]]( fk: F ~> G ): QueryOperationName[G] = QueryOperationName( identifier.map(fk(_)), - fk(operationName).map(_.mapK(fk)), + fk(operationName).map(_.retag[G]), ) } diff --git a/modules/core/src/main/scala/playground/ASTAdapter.scala b/modules/core/src/main/scala/playground/ASTAdapter.scala new file mode 100644 index 00000000..c7d6816f --- /dev/null +++ b/modules/core/src/main/scala/playground/ASTAdapter.scala @@ -0,0 +1,12 @@ +package playground + +import cats.syntax.all.* +import playground.smithyql.QualifiedIdentifier + +object ASTAdapter { + + def decodeQI(qi: playground.generated.nodes.QualifiedIdentifier): Option[QualifiedIdentifier] = + (qi.namespace.map(_.source).toNel, qi.selection.map(_.source)) + .mapN(QualifiedIdentifier.apply) + +} diff --git a/modules/core/src/main/scala/playground/MultiServiceResolver.scala b/modules/core/src/main/scala/playground/MultiServiceResolver.scala index 15c5fc7c..5c088bcb 100644 --- a/modules/core/src/main/scala/playground/MultiServiceResolver.scala +++ b/modules/core/src/main/scala/playground/MultiServiceResolver.scala @@ -9,6 +9,7 @@ import playground.smithyql.UseClause import playground.smithyql.WithSource object MultiServiceResolver { + import playground.smithyql.tsutils.* /** Determines which service should be used for a query. The rules are: * - If the operation name has a service identifier, there MUST be a service with that name @@ -37,6 +38,40 @@ object MultiServiceResolver { case None => resolveImplicit(queryOperationName.operationName, serviceIndex, useClauses) } + /** Determines which service should be used for a query. The rules are: + * - If the operation name has a service identifier, there MUST be a service with that name + * that contains the given operation. + * - If there's no service identifier, find all matching services that are included in the use + * clauses. MUST find exactly one entry. + * + * In other cases, such as when we can't find a unique entry, or the explicitly referenced + * service doesn't have an operation with a matching name, we fail. The latter might eventually + * be refactored to a separate piece of code. + * + * **Important**! + * + * This method assumes that all of the use clauses match the available service set. It does NOT + * perform a check on that. For the actual check, see PreludeCompiler. + */ + def resolveServiceTs( + queryOperationName: playground.generated.nodes.OperationName, + serviceIndex: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, QualifiedIdentifier] = + queryOperationName.name match { + case Some(opName) => + // todo: this should be an option in codegen. might be a bad grammar + queryOperationName.identifier.headOption match { + case Some(explicitRef) => resolveExplicitTs(serviceIndex, explicitRef, opName) + + case None => resolveImplicitTs(opName, serviceIndex, useClauses) + } + case None => + // TODO: operation name is invalid or something like that + ??? + + } + private def resolveExplicit( index: ServiceIndex, explicitRef: WithSource[QualifiedIdentifier], @@ -66,6 +101,41 @@ object MultiServiceResolver { case Some(_) => explicitRef.value.asRight } + private def resolveExplicitTs( + index: ServiceIndex, + explicitRef: playground.generated.nodes.QualifiedIdentifier, + operationName: playground.generated.nodes.Identifier, + ): EitherNel[CompilationError, QualifiedIdentifier] = + ASTAdapter.decodeQI(explicitRef) match { + case None => ??? /* todo - I don't really know xD */ + // explicit reference exists, but doesn't parse + case Some(ref) => + index.getService(ref) match { + // explicit reference exists, but the service doesn't + case None => + CompilationError + .error( + CompilationErrorDetails.UnknownService(index.serviceIds.toList), + explicitRef.range, + ) + .leftNel + + // the service exists, but doesn't have the requested operation + case Some(service) + if !service.operationNames.contains_(OperationName(operationName.source)) => + CompilationError + .error( + CompilationErrorDetails.OperationMissing(service.operationNames.toList), + operationName.range, + ) + .leftNel + + // all good + case Some(_) => ref.asRight + } + + } + private def resolveImplicit( operationName: WithSource[OperationName[WithSource]], index: ServiceIndex, @@ -90,4 +160,29 @@ object MultiServiceResolver { } } + private def resolveImplicitTs( + // todo: introduce type wrapper for OperationName, rename current to QueryOperationName + operationName: playground.generated.nodes.Identifier, + index: ServiceIndex, + useClauses: List[playground.generated.nodes.UseClause], + ): EitherNel[CompilationError, QualifiedIdentifier] = { + val matchingServices = index + .getServices(useClauses.flatMap(_.identifier).flatMap(ASTAdapter.decodeQI).toSet) + .filter(_.hasOperation(OperationName(operationName.source))) + + matchingServices match { + case one :: Nil => one.id.asRight + case _ => + CompilationError + .error( + CompilationErrorDetails + .AmbiguousService( + workspaceServices = index.serviceIds.toList + ), + operationName.range, + ) + .leftNel + } + } + } diff --git a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala index 72cd45d0..87675dca 100644 --- a/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala +++ b/modules/core/src/main/scala/playground/smithyql/RangeIndex.scala @@ -2,7 +2,7 @@ package playground.smithyql import cats.kernel.Monoid import cats.syntax.all.* -import org.polyvariant.treesitter4s.Node +import tsutils.* import util.chaining.* trait RangeIndex { @@ -22,10 +22,6 @@ object RangeIndex { def build(parsed: playground.generated.nodes.SourceFile): RangeIndex = fromRanges { - extension (node: Node) { - def range: SourceRange = SourceRange(Position(node.startByte), Position(node.endByte)) - } - val root = NodeContext.EmptyPath val preludeRanges = parsed @@ -137,12 +133,12 @@ object RangeIndex { allRanges .filter(_.range.contains(pos)) .tap { ranges => - println() - println("=======") - println(s"all ranges: ${allRanges.map(_.render).mkString(", ")}") - println(s"ranges for position ${pos.index}: ${ranges.map(_.render).mkString(", ")}") - println("=======") - println() + // println() + // println("=======") + // println(s"all ranges: ${allRanges.map(_.render).mkString(", ")}") + // println(s"ranges for position ${pos.index}: ${ranges.map(_.render).mkString(", ")}") + // println("=======") + // println() } .maxByOption(_.ctx.length) .map(_.ctx) diff --git a/modules/core/src/main/scala/playground/smithyql/tsutils.scala b/modules/core/src/main/scala/playground/smithyql/tsutils.scala new file mode 100644 index 00000000..5cb55005 --- /dev/null +++ b/modules/core/src/main/scala/playground/smithyql/tsutils.scala @@ -0,0 +1,11 @@ +package playground.smithyql + +import org.polyvariant.treesitter4s.Node + +object tsutils { + + extension (node: Node) { + def range: SourceRange = SourceRange(Position(node.startByte), Position(node.endByte)) + } + +} 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 0a4b934e..8ee26804 100644 --- a/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala +++ b/modules/language-support/src/main/scala/playground/language/CompletionProvider.scala @@ -4,6 +4,7 @@ import cats.Id import cats.kernel.Order.catsKernelOrderingForOrder import cats.syntax.all.* import org.polyvariant.treesitter4s.TreeSitterAPI +import playground.ASTAdapter import playground.MultiServiceResolver import playground.ServiceIndex import playground.smithyql.NodeContext @@ -12,11 +13,7 @@ import playground.smithyql.NodeContext.^^: import playground.smithyql.OperationName import playground.smithyql.Position import playground.smithyql.QualifiedIdentifier -import playground.smithyql.Query import playground.smithyql.RangeIndex -import playground.smithyql.SourceFile -import playground.smithyql.WithSource -import playground.smithyql.parser.SourceParser import playground.smithyql.syntax.* import smithy4s.dynamic.DynamicSchemaIndex @@ -75,15 +72,19 @@ object CompletionProvider { .toList } - def completeRootOperationName( - file: SourceFile[WithSource], + def completeRootOperationNameTs( + file: playground.generated.nodes.SourceFile, insertBodyStruct: CompletionItem.InsertBodyStruct, ) = { // double-check test coverage. // there's definitely a test missing for N>1 clauses. // https://github.com/kubukoz/smithy-playground/issues/161 - val presentServiceIds - : List[QualifiedIdentifier] = file.prelude.useClauses.map(_.value.identifier.value) + val presentServiceIds: List[QualifiedIdentifier] = file + .prelude + .toList + .flatMap(_.use_clause) + .flatMap(_.identifier) + .flatMap(ASTAdapter.decodeQI) // for operations on root level we show: // - completions for ops from the service being used, which don't insert a use clause and don't show the service ID @@ -112,9 +113,9 @@ object CompletionProvider { } // we're definitely in an existing query, so we don't insert a brace in either case. - def completeOperationNameFor( - q: Query[WithSource], - sf: SourceFile[WithSource], + def completeOperationNameForTs( + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, serviceId: Option[QualifiedIdentifier], ): List[CompletionItem] = serviceId match { @@ -122,8 +123,12 @@ object CompletionProvider { // includes the current query's service reference // as it wouldn't result in ading a use clause val presentServiceIdentifiers = - q.operationName.value.mapK(WithSource.unwrap).identifier.toList ++ - sf.prelude.useClauses.map(_.value.identifier.value) + q.operation_name.toList.flatMap(_.identifier).flatMap(ASTAdapter.decodeQI) ++ + sf.prelude + .toList + .flatMap(_.use_clause) + .flatMap(_.identifier) + .flatMap(ASTAdapter.decodeQI) completeOperationName( serviceId, @@ -131,34 +136,38 @@ object CompletionProvider { CompletionItem.InsertBodyStruct.No, ) - case None => completeRootOperationName(sf, CompletionItem.InsertBodyStruct.No) + case None => completeRootOperationNameTs(sf, CompletionItem.InsertBodyStruct.No) } - def completeInQuery( - q: Query[WithSource], - sf: SourceFile[WithSource], + def completeInQueryTs( + q: playground.generated.nodes.RunQuery, + sf: playground.generated.nodes.SourceFile, ctx: NodeContext, - ): List[CompletionItem] = { + ): List[CompletionItem] = q.operation_name.toList.flatMap { operationName => val resolvedServiceId = MultiServiceResolver - .resolveService( - q.operationName.value, + .resolveServiceTs( + operationName, serviceIndex, - sf.prelude.useClauses.map(_.value), + sf.prelude.toList.flatMap(_.use_clause), ) .toOption ctx match { case NodeContext.PathEntry.AtOperationName ^^: EmptyPath => - completeOperationNameFor(q, sf, resolvedServiceId) + completeOperationNameForTs(q, sf, resolvedServiceId) case NodeContext.PathEntry.AtOperationInput ^^: ctx => resolvedServiceId match { case Some(serviceId) => - inputCompletions(serviceId)( - q.operationName.value.operationName.value.mapK(WithSource.unwrap) - ) - .getCompletions(ctx) + q.operation_name + .toList + .flatMap(_.name) + .map(id => OperationName[Id](id.source)) + .flatMap { + inputCompletions(serviceId)(_) + .getCompletions(ctx) + } case None => Nil } @@ -177,44 +186,40 @@ object CompletionProvider { .SourceFile .unsafeApply(TreeSitterAPI.make("smithyql").parse(doc).rootNode.get) - SourceParser[SourceFile].parse(doc) match { - case Left(_) => - // we can try to deal with this later - Nil - - case Right(sf) => - val matchingNode = RangeIndex.build(parsedTs) - .findAtPosition(pos) - .getOrElse(NodeContext.EmptyPath) - - // System.err.println("matchingNode: " + matchingNode.render) - - matchingNode match { - case NodeContext.PathEntry.InQuery(n) ^^: rest => - val q = - sf - .queries(WithSource.unwrap) - .get(n.toLong) - .getOrElse(sys.error(s"Fatal error: no query at index $n")) - .query - .value - - completeInQuery(q, sf, rest) - - case NodeContext.PathEntry.AtPrelude ^^: - NodeContext.PathEntry.AtUseClause(_) ^^: - EmptyPath => - servicesById - .toList - .sortBy(_._1) - .map(CompletionItem.useServiceClause.tupled) - - case EmptyPath => completeRootOperationName(sf, CompletionItem.InsertBodyStruct.Yes) - - case _ => Nil - } + val matchingNode = RangeIndex + .build(parsedTs) + .findAtPosition(pos) + .getOrElse(NodeContext.EmptyPath) + + // System.err.println("matchingNode: " + matchingNode.render) + + matchingNode match { + case NodeContext.PathEntry.InQuery(n) ^^: rest => + val q = parsedTs + .statements + .flatMap(_.run_query) + .get(n.toLong) + .getOrElse(sys.error(s"Fatal error: no query at index $n")) + + completeInQueryTs(q, parsedTs, rest) + + case NodeContext.PathEntry.AtPrelude ^^: + NodeContext.PathEntry.AtUseClause(_) ^^: + EmptyPath => + servicesById + .toList + .sortBy(_._1) + .map(CompletionItem.useServiceClause.tupled) + + case EmptyPath => + completeRootOperationNameTs( + parsedTs, + CompletionItem.InsertBodyStruct.Yes, + ) + case _ => Nil } + } }