Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ object BuildLoader {
case class Loaded(
config: PlaygroundConfig,
configFilePath: Path,
)
) {
def isDummy: Boolean = this == Loaded.default
}

object Loaded {
// Path is irrelevant when no imports are provided.
Expand Down
140 changes: 85 additions & 55 deletions modules/lsp-kernel/src/main/scala/playground/lsp/LanguageServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import playground.language.FormattingProvider
import playground.language.Progress
import playground.language.TextDocumentProvider
import playground.language.TextEdit
import playground.lsp.ServerLoader.PrepareResult
import playground.lsp.buildinfo.BuildInfo
import playground.smithyql.Position
import playground.smithyql.SourceRange
Expand Down Expand Up @@ -116,12 +117,14 @@ object LanguageServer {
)

def instance[
F[_]: {Async, TextDocumentManager, LanguageClient, ServerLoader, CommandResultReporter,
Progress.Make}
F[_]: {Async, TextDocumentManager, LanguageClient, CommandResultReporter, Progress.Make,
ServerLoader.Queue}
](
dsi: DynamicSchemaIndex,
serviceIndex: ServiceIndex,
runner: FileRunner.Resolver[F],
)(
using serverLoader: ServerLoader[F]
): LanguageServer[F] =
new LanguageServer[F] {

Expand Down Expand Up @@ -194,24 +197,35 @@ object LanguageServer {
) *>
ServerLoader[F]
.prepare(workspaceFolders.some)
.flatMap { prepped =>
mkProgress.use { progress =>
progress.report(message = "Loaded build definition from workspace...".some) *>
ServerLoader[F]
.perform(prepped.params, progress)
.flatTap { stats =>
Feedback[F]
.showInfoMessage(
s"Loaded Smithy Playground server with ${stats.render}"
)
}
}
.flatMap { prepared =>
ServerLoader
.Queue[F]
.schedule {
mkProgress.use { progress =>
loadInitialServer(prepared, progress)
}
}
}
.onError { case e => LanguageClient[F].showErrorMessage("Failed to reload project") }
.attempt
.as(InitializeResult(capabilities, serverInfo))
}

private def loadInitialServer(
prepped: PrepareResult[serverLoader.Params],
progress: Progress[F],
) =
progress.report(message = "Loaded build definition from workspace...".some) *>
ServerLoader[F]
.perform(prepped.params, progress)
.flatTap { stats =>
LanguageClient[F].refreshDiagnostics *>
LanguageClient[F].refreshCodeLenses *>
Feedback[F].showInfoMessage(
s"Loaded Smithy Playground server with ${stats.render}"
)
}
.onError { case e => LanguageClient[F].showErrorMessage("Failed to reload project") }
.void

def didChange(
documentUri: Uri,
newText: String,
Expand Down Expand Up @@ -262,19 +276,28 @@ object LanguageServer {

def diagnostic(
documentUri: Uri
): F[List[LSPDiagnostic]] = TextDocumentManager[F]
.get(documentUri)
.map { documentText =>
val diags = diagnosticProvider.getDiagnostics(
documentText
): F[List[LSPDiagnostic]] =
// Due to this happening in the background, we just don't return diagnostics until the server is initialized the first time.
// They'll be refreshed by a server->client request later.
ServerLoader[F]
.isInitialized
.get
.ifM(
ifFalse = Nil.pure[F],
ifTrue = TextDocumentManager[F]
.get(documentUri)
.map { documentText =>
val diags = diagnosticProvider.getDiagnostics(
documentText
)

val map = LocationMap(documentText)

diags
.map(LSPDiagnostic(_, map))
},
)

val map = LocationMap(documentText)

diags
.map(LSPDiagnostic(_, map))
}

def definition(documentUri: Uri, position: LSPPosition): F[List[LSPLocation]] =
TextDocumentManager[F]
.get(documentUri)
Expand Down Expand Up @@ -318,34 +341,41 @@ object LanguageServer {
DocumentSymbolProvider.make(text).map(LSPDocumentSymbol(_, map))
}

def didChangeWatchedFiles: F[Unit] = ServerLoader[F]
.prepare(workspaceFolders = None)
.flatMap {
case prepared if !prepared.isChanged =>
Feedback[F].showInfoMessage(
"No change detected, not rebuilding server"
)
case prepared =>
Feedback[F].showInfoMessage("Detected changes, will try to rebuild server...") *>
Progress
.create(title = "Rebuilding server", message = None)
.use {
ServerLoader[F]
.perform(prepared.params, _)
}
.onError { case e =>
LanguageClient[F].showErrorMessage(
"Couldn't reload server: " + e.getMessage
)
}
.flatMap { stats =>
LanguageClient[F].refreshDiagnostics *>
LanguageClient[F].refreshCodeLenses *>
Feedback[F].showInfoMessage(
s"Reloaded Smithy Playground server with ${stats.render}"
)
}
}
def didChangeWatchedFiles: F[Unit] =
// Q: is this safe? or should we do this in the background / with a mutex of sorts?
ServerLoader[F].isInitialized.waitUntil(identity) *>
ServerLoader[F]
.prepare(workspaceFolders = None)
.flatMap {
case prepared if !prepared.isChanged =>
Feedback[F].showInfoMessage(
"No change detected, not rebuilding server"
)

case prepared => reloadServer(prepared)
}

private def reloadServer(prepared: PrepareResult[serverLoader.Params]) = {
Feedback[F].showInfoMessage("Detected changes, will try to rebuild server...") *>
Progress
.create(title = "Rebuilding server", message = None)
.use {
ServerLoader[F]
.perform(prepared.params, _)
}
.onError { case e =>
LanguageClient[F].showErrorMessage(
"Couldn't reload server: " + e.getMessage
)
}
.flatMap { stats =>
LanguageClient[F].refreshDiagnostics *>
LanguageClient[F].refreshCodeLenses *>
Feedback[F].showInfoMessage(
s"Reloaded Smithy Playground server with ${stats.render}"
)
}
}
.onError { case e =>
LanguageClient[F].showErrorMessage(
s"Couldn't rebuild server. Check your config file and the output panel.\nError: ${e.getMessage()}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ object ServerBuilder {
.memoize
given TextDocumentManager[F] <- TextDocumentManager.instance[F].toResource
rep <- CommandResultReporter.instance[F].toResource
given ServerLoader.Queue[F] <- ServerLoader.Queue.createCancelable[F]

} yield new ServerBuilder[F] {
given Environment[F] = LSPEnvironment.instance[F](using httpClient =
httpClient
given Environment[F] = LSPEnvironment.instance[F](
using httpClient = httpClient
)

override def build(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package playground.lsp

import cats.MonadThrow
import cats.effect.kernel.Concurrent
import cats.effect.kernel.Ref
import cats.effect.kernel.Resource
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.concurrent.Signal
import fs2.concurrent.SignallingRef
import playground.PlaygroundConfig
import playground.Uri
import playground.language.Feedback
import playground.language.Progress

trait ServerLoader[F[_]] {
Expand All @@ -21,10 +25,36 @@ trait ServerLoader[F[_]] {
): F[ServerLoader.WorkspaceStats]

def server: LanguageServer[F]
def isInitialized: Signal[F, Boolean]
}

object ServerLoader {

trait Queue[F[_]] {
def schedule(task: F[Unit]): F[Unit]
}

object Queue {

def apply[F[_]](
implicit F: Queue[F]
): Queue[F] = F

def createCancelable[F[_]: Concurrent]
: Resource[F, Queue[F]] = cats.effect.std.Queue.synchronous[F, F[Unit]].toResource.flatMap {
q =>
fs2
.Stream
.fromQueueUnterminated(q)
.switchMap(fs2.Stream.exec)
.compile
.drain
.background
.as(q.offer(_))
}

}

def apply[F[_]](
implicit F: ServerLoader[F]
): F.type = F
Expand Down Expand Up @@ -61,7 +91,7 @@ object ServerLoader {

}

def instance[F[_]: ServerBuilder: BuildLoader: Feedback: Ref.Make: MonadThrow]
def instance[F[_]: ServerBuilder: BuildLoader: Concurrent]
: F[ServerLoader.Aux[F, BuildLoader.Loaded]] = {
case class State(
currentServer: LanguageServer[F],
Expand All @@ -72,7 +102,7 @@ object ServerLoader {
}

(
Ref[F]
SignallingRef[F]
.of(State.initial),
Ref[F].of(Option.empty[List[Uri]]),
)
Expand Down Expand Up @@ -116,10 +146,15 @@ object ServerLoader {
.as(WorkspaceStats.fromPlaygroundConfig(params.config))

val server: LanguageServer[F] = LanguageServer.defer(stateRef.get.map(_.currentServer))

val isInitialized: Signal[F, Boolean] = stateRef.map(
_.lastUsedConfig.exists(!_.isDummy)
)
}
}
.flatTap { serverLoader =>
// loading with dummy config to initialize server without dependencies
// we don't report progress either, as this is a very quick / uninteresting operation
serverLoader.perform(BuildLoader.Loaded.default, Progress.ignored[F])
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ trait LanguageServerIntegrationTests {
progressToken = Some("init-progress"),
clientCapabilities = ClientCapabilities(windowProgress = true),
) *>
// problem: initialize is now async, so we need something to wait for.
// this would require some changes to the LanguageServer API which smells like a bit of a leak.
assertStartupEvents(client)
.as(result)
}
Expand Down
Loading