Skip to content

Commit

Permalink
Merge pull request #60 from kubukoz/reorg-reload
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Jul 31, 2022
2 parents 6c3cba4 + 3906832 commit 1652e03
Show file tree
Hide file tree
Showing 13 changed files with 540 additions and 266 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is an experimental, work-in-progress project to develop a query language for Smithy.

Currently, everything is only available as a VS Code plugin, with future ideas to extract a Language Server.
Currently available as a LSP server with a client implementation for VS Code.

## Usage

Expand Down
10 changes: 0 additions & 10 deletions core/src/main/scala/playground/CommandProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@ import playground.smithyql.WithSource
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.immutable.ListMap

trait Feedback[F[_]] {
def showErrorMessage(msg: String): F[Unit]
def showOutputPanel: F[Unit]
def logOutput(msg: String): F[Unit]
}

object Feedback {
def apply[F[_]](implicit F: Feedback[F]): Feedback[F] = F
}

trait CommandProvider[F[_]] {
def runCommand(name: String, args: List[String]): F[Unit]
}
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/scala/playground/Feedback.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package playground

//todo: remove in favor of command listener/reporter
trait Feedback[F[_]] {
def showInfoMessage(msg: String): F[Unit]
def showErrorMessage(msg: String): F[Unit]
def showOutputPanel: F[Unit]
def logOutput(msg: String): F[Unit]
}

object Feedback {
def apply[F[_]](implicit F: Feedback[F]): Feedback[F] = F
}
6 changes: 3 additions & 3 deletions core/src/main/scala/playground/run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.Id
import cats.data.Ior
import cats.data.IorNel
import cats.data.NonEmptyList
import cats.effect.Async
import cats.effect.Concurrent
import cats.effect.MonadCancelThrow
import cats.effect.Resource
import cats.effect.implicits._
Expand Down Expand Up @@ -249,7 +249,7 @@ object Runner {
}
}

def forSchemaIndex[F[_]: Async: std.Console](
def forSchemaIndex[F[_]: Concurrent: Defer: std.Console](
dsi: DynamicSchemaIndex,
client: Client[F],
baseUri: F[Uri],
Expand Down Expand Up @@ -293,7 +293,7 @@ object Runner {
}
}

def forService[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]: Async: std.Console](
def forService[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]: Concurrent: Defer: std.Console](
service: Service[Alg, Op],
client: Client[F],
baseUri: F[Uri],
Expand Down
105 changes: 105 additions & 0 deletions lsp/src/main/scala/playground/lsp/BuildLoader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package playground.lsp

import cats.effect.kernel.Sync
import cats.implicits._
import fs2.io.file.Path
import playground.BuildConfig
import playground.BuildConfigDecoder
import playground.ModelReader
import playground.TextDocumentProvider
import smithy4s.codegen.ModelLoader
import smithy4s.dynamic.DynamicSchemaIndex

trait BuildLoader[F[_]] {
def load: F[BuildLoader.Loaded]

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

}

object BuildLoader {
def apply[F[_]](implicit F: BuildLoader[F]): BuildLoader[F] = F

case class Loaded(config: BuildConfig, configFilePath: Path)

def instance[F[_]: TextDocumentProvider: Sync]: BuildLoader[F] =
new BuildLoader[F] {

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

fs2
.Stream
.emit(Path("."))
.flatMap { folder =>
fs2
.Stream
.emits(configFiles)
.map(folder.resolve(_).absolute)
}
.evalMap(filePath =>
TextDocumentProvider[F]
.getOpt(
filePath
.toNioPath
.toUri()
.toString()
)
.map(_.tupleRight(filePath))
)
.unNone
.head
.compile
.last
.flatMap {
_.liftTo[F](
new Throwable(
"Couldn't find one of the following files: " + configFiles.mkString(", ")
)
)
}
.flatMap { case (fileContents, filePath) =>
BuildConfigDecoder
.decode(fileContents.getBytes())
.liftTo[F]
.tupleRight(filePath)
.map(BuildLoader.Loaded.apply.tupled)
}
}

def buildSchemaIndex(loaded: BuildLoader.Loaded): F[DynamicSchemaIndex] = Sync[F]
.interruptibleMany {
ModelLoader
.load(
specs =
loaded
.config
.imports
.combineAll
.map(
loaded
.configFilePath
.parent
.getOrElse(sys.error("impossible - no parent"))
.resolve(_)
.toNioPath
.toFile()
)
.toSet,
dependencies = loaded.config.mavenDependencies.combineAll,
repositories = loaded.config.mavenRepositories.combineAll,
transformers = Nil,
discoverModels = false,
localJars = Nil,
)
._2
}
.flatMap(ModelReader.buildSchemaIndex[F])

}

}
28 changes: 19 additions & 9 deletions lsp/src/main/scala/playground/lsp/LanguageClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ import playground.Feedback
import java.util.concurrent.CompletableFuture
import scala.jdk.CollectionConverters._
import scala.util.chaining._
import cats.FlatMap

trait LanguageClient[F[_]] extends Feedback[F] {
def configuration[A: Decoder](section: String): F[A]
def showMessage(tpe: MessageType, msg: String): F[Unit]
def refreshDiagnostics: F[Unit]
def refreshCodeLenses: F[Unit]

def showInfoMessage(msg: String): F[Unit] = showMessage(MessageType.Info, msg)
def showErrorMessage(msg: String): F[Unit] = showMessage(MessageType.Error, msg)
}

object LanguageClient {
Expand Down Expand Up @@ -54,8 +61,8 @@ object LanguageClient {
}
.flatMap(_.as[A].liftTo[F])

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

def logOutput(msg: String): F[Unit] = withClientSync(
Expand All @@ -64,20 +71,23 @@ object LanguageClient {

def showOutputPanel: F[Unit] = withClientSync(_.showOutputPanel())

def refreshCodeLenses: F[Unit] = withClientF(_.refreshCodeLenses()).void
def refreshDiagnostics: F[Unit] = withClientF(_.refreshDiagnostics()).void
}

def suspend[F[_]: Async](clientF: F[LanguageClient[F]]): LanguageClient[F] =
def defer[F[_]: FlatMap](fa: F[LanguageClient[F]]): LanguageClient[F] =
new LanguageClient[F] {
def showOutputPanel: F[Unit] = fa.flatMap(_.showOutputPanel)

def showErrorMessage(msg: String): F[Unit] = clientF.flatMap(_.showErrorMessage(msg))
def logOutput(msg: String): F[Unit] = fa.flatMap(_.logOutput(msg))

def logOutput(msg: String): F[Unit] = clientF.flatMap(_.logOutput(msg))
def configuration[A: Decoder](section: String): F[A] = fa.flatMap(_.configuration(section))

def configuration[A: Decoder](
section: String
): F[A] = clientF.flatMap(_.configuration(section))
def showMessage(tpe: MessageType, msg: String): F[Unit] = fa.flatMap(_.showMessage(tpe, msg))

def refreshDiagnostics: F[Unit] = fa.flatMap(_.refreshDiagnostics)

val showOutputPanel: F[Unit] = clientF.flatMap(_.showOutputPanel)
def refreshCodeLenses: F[Unit] = fa.flatMap(_.refreshCodeLenses)

}

Expand Down
103 changes: 98 additions & 5 deletions lsp/src/main/scala/playground/lsp/LanguageServer.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package playground.lsp

import cats.Applicative
import cats.FlatMap
import cats.MonadThrow
import cats.effect.implicits._
import cats.effect.kernel.Async
import cats.effect.std.Supervisor
import cats.implicits._
import cats.~>
import com.google.gson.JsonElement
Expand All @@ -25,6 +29,7 @@ import scala.util.chaining._

trait LanguageServer[F[_]] {
def initialize(params: InitializeParams): F[InitializeResult]
def initialized(params: InitializedParams): F[Unit]
def didChange(params: DidChangeTextDocumentParams): F[Unit]
def didOpen(params: DidOpenTextDocumentParams): F[Unit]
def didSave(params: DidSaveTextDocumentParams): F[Unit]
Expand All @@ -33,16 +38,27 @@ trait LanguageServer[F[_]] {
def completion(position: CompletionParams): F[Either[List[CompletionItem], CompletionList]]
def diagnostic(params: DocumentDiagnosticParams): F[DocumentDiagnosticReport]
def codeLens(params: CodeLensParams): F[List[CodeLens]]

def didChangeWatchedFiles(
params: DidChangeWatchedFilesParams
): F[Unit]

def executeCommand(params: ExecuteCommandParams): F[Unit]
def shutdown(): F[Unit]
def exit(): F[Unit]
def shutdown: F[Unit]
def exit: F[Unit]
}

object LanguageServer {

def instance[F[_]: Async: TextDocumentManager: LanguageClient](
def notAvailable[F[_]: MonadThrow]: LanguageServer[F] = defer(
new Throwable("Server not available").raiseError[F, LanguageServer[F]]
)

def instance[F[_]: Async: TextDocumentManager: LanguageClient: ServerLoader](
dsi: DynamicSchemaIndex,
runner: Runner.Optional[F],
)(
implicit sup: Supervisor[F]
): LanguageServer[F] =
new LanguageServer[F] {

Expand Down Expand Up @@ -71,7 +87,8 @@ object LanguageServer {
new InitializeResult(capabilities).pure[F]
}

def shutdown(): F[Unit] = Applicative[F].unit
def initialized(params: InitializedParams): F[Unit] = Applicative[F].unit
def shutdown: F[Unit] = Applicative[F].unit

def didChange(params: DidChangeTextDocumentParams): F[Unit] = {
val changesAsList = params.getContentChanges.asScala.toList
Expand Down Expand Up @@ -175,6 +192,37 @@ object LanguageServer {
.map(converters.toLSP.codeLens(documentText, _))
}

def didChangeWatchedFiles(
params: DidChangeWatchedFilesParams
): F[Unit] = ServerLoader[F].prepare.flatMap {
case prepared if !prepared.isChanged =>
LanguageClient[F].showInfoMessage(
"No change detected, not rebuilding server"
)
case prepared =>
LanguageClient[F].showInfoMessage("Detected changes, will try to rebuild server...") *>
ServerLoader[F]
.perform(prepared.params)
.onError { case e =>
LanguageClient[F].showErrorMessage(
"Couldn't reload server: " + e.getMessage
)
}
.flatMap { stats =>
// Can't make (and wait for) client requests while handling a client request (file change)
{
LanguageClient[F].refreshDiagnostics *>
LanguageClient[F].refreshCodeLenses *> LanguageClient[F]
.showInfoMessage(
s"Reloaded Smithy Playground server with " +
s"${stats.importCount} imports, " +
s"${stats.dependencyCount} dependencies and " +
s"${stats.pluginCount} plugins"
)
}.supervise(sup).void
}
}

def executeCommand(
params: ExecuteCommandParams
): F[Unit] = params
Expand All @@ -187,7 +235,52 @@ object LanguageServer {
}
.flatMap(commandProvider.runCommand(params.getCommand(), _))

def exit(): F[Unit] = Applicative[F].unit
def exit: F[Unit] = Applicative[F].unit
}

def defer[F[_]: FlatMap](
fk: F[LanguageServer[F]]
): LanguageServer[F] =
new LanguageServer[F] {

def initialize(
params: InitializeParams
): F[InitializeResult] = fk.flatMap(_.initialize(params))

def initialized(params: InitializedParams): F[Unit] = fk.flatMap(_.initialized(params))

def didChange(params: DidChangeTextDocumentParams): F[Unit] = fk.flatMap(_.didChange(params))

def didOpen(params: DidOpenTextDocumentParams): F[Unit] = fk.flatMap(_.didOpen(params))

def didSave(params: DidSaveTextDocumentParams): F[Unit] = fk.flatMap(_.didSave(params))

def didClose(params: DidCloseTextDocumentParams): F[Unit] = fk.flatMap(_.didClose(params))

def formatting(
params: DocumentFormattingParams
): F[List[TextEdit]] = fk.flatMap(_.formatting(params))

def completion(position: CompletionParams): F[Either[List[CompletionItem], CompletionList]] =
fk.flatMap(_.completion(position))

def diagnostic(
params: DocumentDiagnosticParams
): F[DocumentDiagnosticReport] = fk.flatMap(_.diagnostic(params))

def codeLens(params: CodeLensParams): F[List[CodeLens]] = fk.flatMap(_.codeLens(params))

def didChangeWatchedFiles(
params: DidChangeWatchedFilesParams
): F[Unit] = fk.flatMap(_.didChangeWatchedFiles(params))

def executeCommand(
params: ExecuteCommandParams
): F[Unit] = fk.flatMap(_.executeCommand(params))

def shutdown: F[Unit] = fk.flatMap(_.shutdown)

def exit: F[Unit] = fk.flatMap(_.exit)
}

}
Loading

0 comments on commit 1652e03

Please sign in to comment.