Skip to content

Commit

Permalink
Server integration tests, support workspace directory (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Nov 1, 2022
1 parent 5204415 commit 1620250
Show file tree
Hide file tree
Showing 33 changed files with 766 additions and 123 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/target
.direnv/
.idea/
.vscode/
**/.DS_Store
.smithy.lsp.log
.sbt
Expand Down
2 changes: 2 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ optIn.breakChainOnFirstMethodDot = true
includeCurlyBraceInSelectChains = true
includeNoParensInSelectChains = true

assumeStandardLibraryStripMargin = true

trailingCommas = "multiple"

rewrite.rules = [
Expand Down
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Contributing

Not much to be found here, but feel free to extend this document with anything useful.

## Infrastructure

There are two main infrastructure components:

- the language client: a VS Code extension using the `vscode-languageclient` library to start and communicate with a language server (using Coursier to fetch and run the jars)
- the langauge server: a Scala (JVM) application using the [`lsp4j`](https://github.com/eclipse/lsp4j) library to implement the LSP protocol.

The communication happens over standard I/O. Stdout (your `println` inside the server) is redirected to the logfile,`smithyql-log.txt` inside the workspace. Note: it might not work, so I suggest you write to `System.err` or use Main's `logOut` directly.

## Resources

- [lsp4j documentation](https://github.com/eclipse/lsp4j/blob/main/documentation/README.md)
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,14 @@ lazy val lsp = module("lsp")
"org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.16.0",
"io.circe" %% "circe-core" % "0.14.3",
"org.http4s" %% "http4s-ember-client" % "0.23.16",
"org.http4s" %% "http4s-ember-server" % "0.23.16" % Test,
"io.get-coursier" %% "coursier" % "2.0.16",
"org.typelevel" %% "cats-tagless-macros" % "0.14.0",
),
buildInfoPackage := "playground.lsp.buildinfo",
buildInfoKeys ++= Seq(version),
Smithy4sCodegenPlugin.defaultSettings(Test),
Test / smithy4sSmithyLibrary := false,
)
.enablePlugins(BuildInfoPlugin)
.dependsOn(languageSupport)
Expand Down
9 changes: 6 additions & 3 deletions modules/core/src/main/scala/playground/BuildConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import smithy4s.http.PayloadError

object BuildConfigDecoder {

val decode: Array[Byte] => Either[PayloadError, BuildConfig] = {
private val codec
: (Array[Byte] => Either[PayloadError, BuildConfig], BuildConfig => Array[Byte]) = {
val capi = smithy4s.http.json.codecs()

val codec = capi.compileCodec(BuildConfig.schema)

capi.decodeFromByteArray(codec, _)
(capi.decodeFromByteArray(codec, _: Array[Byte]), capi.writeToArray(codec, _: BuildConfig))
}

val decode: Array[Byte] => Either[PayloadError, BuildConfig] = codec._1
val encode: BuildConfig => Array[Byte] = codec._2

}
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ object CompilationTests extends SimpleIOSuite with Checkers {
pureTest("deprecated service's use clause") {
parseAndCompile(DeprecatedServiceGen)(
"""use service demo.smithy#LiterallyAnyService
|hello {}""".stripMargin
|hello {}""".stripMargin
).left match {
case Some(cf: CompilationFailed) =>
val result = cf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import playground.smithyql.parser.SourceParser
import playground.smithyql.Query

trait CodeLensProvider[F[_]] {
def provide(documentUri: String, documentText: String): List[CodeLens]
def provide(documentUri: Uri, documentText: String): List[CodeLens]
}

object CodeLensProvider {
Expand All @@ -20,7 +20,7 @@ object CodeLensProvider {
): CodeLensProvider[F] =
new CodeLensProvider[F] {

def provide(documentUri: String, documentText: String): List[CodeLens] =
def provide(documentUri: Uri, documentText: String): List[CodeLens] =
SourceParser[Query].parse(documentText) match {
case Right(parsed) if runner.get(parsed).toEither.isRight =>
compiler
Expand All @@ -31,7 +31,7 @@ object CodeLensProvider {
Command(
title = "Run query",
command = Command.RUN_QUERY,
args = documentUri :: Nil,
args = documentUri.value :: Nil,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object CommandProvider {
): CommandProvider[F] =
new CommandProvider[F] {

private def runQuery(documentUri: String): F[Unit] = TextDocumentProvider[F]
private def runQuery(documentUri: Uri): F[Unit] = TextDocumentProvider[F]
.get(documentUri)
.flatMap { documentText =>
SourceParser[Query]
Expand Down Expand Up @@ -67,7 +67,7 @@ object CommandProvider {

private val commandMap: Map[String, List[String] => F[Unit]] = ListMap(
Command.RUN_QUERY -> {
case documentUri :: Nil => runQuery(documentUri)
case documentUri :: Nil => runQuery(Uri.fromUriString(documentUri))
case s => new Throwable("Unsupported arguments: " + s).raiseError[F, Unit]
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ object DiagnosticProvider {
.supported
.map(_.show)
.mkString_(", ")}.
|Found protocols: ${ps.found.map(_.show).mkString(", ")}
|Running queries will not be possible.""".stripMargin,
|Found protocols: ${ps.found.map(_.show).mkString(", ")}
|Running queries will not be possible.""".stripMargin,
pos,
)
)
Expand All @@ -73,7 +73,7 @@ object DiagnosticProvider {
List(
info(
s"""Service unsupported. Running queries will not be possible.
|Details: $e""".stripMargin,
|Details: $e""".stripMargin,
pos,
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package playground.language

trait TextDocumentProvider[F[_]] {
def get(uri: String): F[String]
def getOpt(uri: String): F[Option[String]]
def get(uri: Uri): F[String]
def getOpt(uri: Uri): F[Option[String]]
}

object TextDocumentProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package playground.language

import fs2.io.file.Path

import java.nio.file.Paths
import java.net.URI

final case class Uri private (value: String) extends AnyVal {
def toPath: Path = Path.fromNioPath(Paths.get(new URI(value)))

// :/
// only for tests!
def /(subdir: String): Uri = Uri(value + "/" + subdir)
}

object Uri {
def fromPath(path: Path): Uri = fromUriString(path.toNioPath.toUri().toString())
def fromUriString(s: String): Uri = new Uri(s)
}
21 changes: 10 additions & 11 deletions modules/lsp/src/main/scala/playground/lsp/BuildLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import playground.ModelReader
import playground.language.TextDocumentProvider
import smithy4s.codegen.ModelLoader
import smithy4s.dynamic.DynamicSchemaIndex
import playground.language.Uri

trait BuildLoader[F[_]] {
def load: F[BuildLoader.Loaded]
def load(workspaceFolders: List[Uri]): F[BuildLoader.Loaded]

def buildSchemaIndex(info: BuildLoader.Loaded): F[DynamicSchemaIndex]

Expand All @@ -30,16 +31,19 @@ object BuildLoader {
def instance[F[_]: TextDocumentProvider: Sync]: BuildLoader[F] =
new BuildLoader[F] {

def load: F[BuildLoader.Loaded] = {
def load(workspaceFolders: List[Uri]): F[BuildLoader.Loaded] = {
val configFiles = List(
"build/smithy-dependencies.json",
".smithy.json",
"smithy-build.json",
)

// For now, we only support a single workspace folder.
fs2
.Stream
.emit(Path("."))
.emit(
workspaceFolders.headOption.getOrElse(sys.error("no workspace folders found")).toPath
)
.flatMap { folder =>
fs2
.Stream
Expand All @@ -48,12 +52,7 @@ object BuildLoader {
}
.evalMap(filePath =>
TextDocumentProvider[F]
.getOpt(
filePath
.toNioPath
.toUri()
.toString()
)
.getOpt(Uri.fromPath(filePath))
.map(_.tupleRight(filePath))
)
.unNone
Expand All @@ -71,8 +70,7 @@ object BuildLoader {
BuildConfigDecoder
.decode(fileContents.getBytes())
.liftTo[F]
.tupleRight(filePath)
.map(BuildLoader.Loaded.apply.tupled)
.map(BuildLoader.Loaded.apply(_, filePath))
}
}

Expand All @@ -98,6 +96,7 @@ object BuildLoader {
dependencies = loaded.config.mavenDependencies.combineAll,
repositories = loaded.config.mavenRepositories.combineAll,
transformers = Nil,
// todo: this should be false really
discoverModels = true,
localJars = Nil,
)
Expand Down
41 changes: 41 additions & 0 deletions modules/lsp/src/main/scala/playground/lsp/ConfigurationValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package playground.lsp

import io.circe.Codec
import io.circe.Encoder
import io.circe.Decoder
import org.http4s.Uri
import cats.implicits._
import io.circe.Json

trait ConfigurationValue[T] {
def key: String
def codec: Codec[T]
def apply(value: T): ConfigurationValue.Applied[T] = ConfigurationValue.Applied(this, value)
}

object ConfigurationValue {

def make[A: Encoder: Decoder](k: String): ConfigurationValue[A] =
new ConfigurationValue[A] {
val key: String = k
val codec: Codec[A] = Codec.from(implicitly, implicitly)
}

final case class Applied[T](cv: ConfigurationValue[T], value: T) {
def encoded: Json = cv.codec.apply(value)
}

implicit val uriJsonDecoder: Decoder[Uri] = Decoder[String].emap(
Uri.fromString(_).leftMap(_.message)
)

implicit val uriJsonEncoder: Encoder[Uri] = Encoder[String].contramap(_.renderString)

val maxWidth: ConfigurationValue[Int] = make[Int]("smithyql.formatter.maxWidth")
val baseUri: ConfigurationValue[Uri] = make[Uri]("smithyql.http.baseUrl")

val authorizationHeader: ConfigurationValue[String] = make[String](
"smithyql.http.authorizationHeader"
)

}
11 changes: 4 additions & 7 deletions modules/lsp/src/main/scala/playground/lsp/LanguageClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import cats.tagless.Derive
import cats.tagless.FunctorK
import cats.tagless.implicits._
import com.google.gson.JsonElement
import io.circe.Decoder
import org.eclipse.lsp4j.ConfigurationItem
import org.eclipse.lsp4j.ConfigurationParams
import org.eclipse.lsp4j.MessageParams
Expand All @@ -20,7 +19,7 @@ import scala.jdk.CollectionConverters._
import scala.util.chaining._

trait LanguageClient[F[_]] extends Feedback[F] {
def configuration[A: Decoder](section: String): F[A]
def configuration[A](v: ConfigurationValue[A]): F[A]
def showMessage(tpe: MessageType, msg: String): F[Unit]
def refreshDiagnostics: F[Unit]
def refreshCodeLenses: F[Unit]
Expand All @@ -46,12 +45,10 @@ object LanguageClient {
f: client.type => A
): F[A] = Async[F].delay(f(client))

def configuration[A: Decoder](
section: String
): F[A] = withClientF(
def configuration[A](v: ConfigurationValue[A]): F[A] = withClientF(
_.configuration(
new ConfigurationParams(
(new ConfigurationItem().tap(_.setSection(section)) :: Nil).asJava
(new ConfigurationItem().tap(_.setSection(v.key)) :: Nil).asJava
)
)
)
Expand All @@ -65,7 +62,7 @@ object LanguageClient {
case e: JsonElement => converters.gsonToCirce(e)
case e => throw new RuntimeException(s"Unexpected configuration value: $e")
}
.flatMap(_.as[A].liftTo[F])
.flatMap(_.as[A](v.codec).liftTo[F])

def showMessage(tpe: MessageType, msg: String): F[Unit] = withClientSync(
_.showMessage(new MessageParams(tpe, msg))
Expand Down
Loading

0 comments on commit 1620250

Please sign in to comment.