Skip to content

Commit

Permalink
Auto-bind to random ports and show messages to client (#30)
Browse files Browse the repository at this point in the history
* Auto-bind to random ports and show messages to client

* Test that summary is served and fix tests

* Set window title

* Update docs
  • Loading branch information
keynmol authored Aug 27, 2022
1 parent 165ed46 commit 42ac8aa
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 43 deletions.
23 changes: 17 additions & 6 deletions docs/_docs/tracer.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ It doesn't matter what the target LSP is implemented with, as we only intercept

Currently, primary method of distribution is via [Coursier](https://get-coursier.io/docs/overview).

To integrate with an LSP of your choosing, you need to have access to the CLI command that launches is.
To integrate with an LSP of your choosing, you need to have access to the CLI command that launches it.

The principle remains the same regardless of the editor:

Expand All @@ -29,7 +29,7 @@ cs boostrap tech.neander:langoustine-tracer_3:latest.release -f -o langoustine-t
# now you can use ./langoustine-tracer,
# put it somewhere on your PATH so that it's globally avalable
# and use it like this:
langoustine-tracer --port 9911 my-awesome lsp --stdin true
langoustine-tracer my-awesome lsp --stdin true
```

Alternatively, if your system was setup with coursier (i.e. the path where it puts
Expand All @@ -48,14 +48,11 @@ For example, if your command to launch the LSP is `my-awesome lsp --stdin true`,
langoustine-tracer my-awesome lsp --stdin true
```

Tracer will interpret everything after the **first `--`** as the command that launches your LSP.
Everything **before first `--`** will be used to configure the tracer itself.

## Tips

### Changing port

By default, it will start the server at the port **9977** but you can change that using the `--port` argument:
By default, it will start the server at random port, but you can change that using the `--port` argument:

```
langoustine-tracer --port 9911 my-awesome lsp --stdin true
Expand All @@ -70,3 +67,17 @@ alias langoustine-tracer=<path-to-langoustine>/modules/tracer/backend/target/jvm
```

That launcher is created by running `sbt tracer/stage`

If your focus is on the frontend, you can speed up your development
cycle considerably by doing the following:

1. Start your traced LSP somehow (doesn't matter if it's a coursier-installed one
or your local)
2. Take note of the port number
3. `cd modules/tracer/frontend`
4. `LANGOUSTINE_PORT=<port> npm run dev`
5. In a separate terminal, run `sbt ~tracerFrontendJS/fastLinkJS`

Now you can edit just the frontend code and Vite will automatically refresh
the page - no need to restart the LSP to pick up changes.

2 changes: 1 addition & 1 deletion modules/lsp/src/main/scala/JSONRPC.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import upickle.default.*

import util.chaining.*

private[lsp] object jsonrpcIntegration:
private[langoustine] object jsonrpcIntegration:
given codec[T: Reader: Writer]: Codec[T] =
new Codec[T]:
override def decode(
Expand Down
3 changes: 1 addition & 2 deletions modules/tracer/backend/src/main/scala/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ object Config:
val portHelo = "Port to start Tracer on"
Opts
.option[Int]("port", portHelo)
.orElse(Opts.env[Int]("PORT", portHelo))
.withDefault(9977)
.withDefault(0)

val lsp = Opts.arguments[String]("lspCommand")

Expand Down
63 changes: 51 additions & 12 deletions modules/tracer/backend/src/main/scala/Tracer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import org.http4s.EntityEncoder
import jsonrpclib.Payload
import jsonrpclib.ErrorPayload
import jsonrpclib.CallId
import langoustine.lsp.requests.textDocument
import langoustine.lsp.requests.window
import langoustine.lsp.structures.ShowMessageParams
import com.github.plokhotnyuk.jsoniter_scala.core.*

object Tracer extends IOApp:
def run(args: List[String]): IO[ExitCode] =
Expand All @@ -29,12 +33,43 @@ def Launch(config: Config) =
val byteTopic =
Resource.eval(fs2.concurrent.Channel.synchronous[IO, Chunk[Byte]])

val in = fs2.io.stdin[IO](512)
val out = fs2.io.stdout[IO]
val latch =
Resource.eval(IO.deferred[org.http4s.Uri])

(byteTopic, byteTopic, byteTopic).parTupled
.flatMap { case (inBytes, outBytes, errBytes) =>
val in = fs2.io.stdin[IO](512)
val out = fs2.io.stdout[IO]
val currentFolder = System.getProperty("user.dir")
val currentCommand = config.cmd.toList
val summary = Summary(currentFolder, currentCommand)

(byteTopic, byteTopic, byteTopic, latch).parTupled
.flatMap { case (inBytes, outBytes, errBytes, outLatch) =>
ChildProcess.resource[IO](config.cmd.toList*).flatMap { child =>

import langoustine.lsp.jsonrpcIntegration.given

def msg(uri: org.http4s.Uri) =
s"Langoustine Tracer started at ${uri}"

def sendHello(uri: org.http4s.Uri) =
val params = ShowMessageParams(
langoustine.lsp.enumerations.MessageType.Info,
msg(uri)
)
val serialised = jsonrpclib.Codec.encode(params)
val asStr = writeToStringReentrant(serialised)
val preamble =
s"""{"method": "window/showMessage", "params": $asStr}"""
val length = preamble.getBytes.length
val message = s"Content-Length: $length\r\n\r\n$preamble"

fs2.Stream
.chunk(Chunk.array(message.getBytes))
.through(out)
.compile
.drain
end sendHello

val redirectInput =
in.chunks
.evalTap(inBytes.send)
Expand All @@ -45,13 +80,13 @@ def Launch(config: Config) =
.background

val redirectOutput =
child.stdout.chunks
.evalTap(outBytes.send)
.unchunks
.through(out)
.compile
.drain
.background
(outLatch.get.flatMap(sendHello) *>
child.stdout.chunks
.evalTap(outBytes.send)
.unchunks
.through(out)
.compile
.drain).background

val redirectLogs =
child.stderr.chunks
Expand All @@ -66,7 +101,11 @@ def Launch(config: Config) =
outBytes.stream.unchunks,
errBytes.stream.unchunks
)
.runResource(config)
.runResource(config, summary)
.evalTap(server =>
IO.consoleForIO.errorln(msg(server.baseUri)) *>
outLatch.complete(server.baseUri)
)

(redirectInput, redirectOutput, redirectLogs, server).parTupled
}
Expand Down
13 changes: 7 additions & 6 deletions modules/tracer/backend/src/main/scala/TracerServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,12 @@ class TracerServer private (
ch <- Channel.bounded[IO, String](128)
yield State(ch, rf, raw, logBuf)

def runStream(config: Config) =
fs2.Stream.resource(runResource(config))

def runResource(config: Config) =
def runResource(config: Config, summary: Summary) =
Resource.eval(State.create).flatMap { state =>
val run = Server(
wbs =>
ErrorHandling
.httpApp(handleErrors(app(wbs, state))),
.httpApp(handleErrors(app(wbs, state, summary))),
config
)
val dump = dumpRequests(state)
Expand All @@ -147,10 +144,14 @@ class TracerServer private (

def app(
wbs: WebSocketBuilder2[IO],
state: State
state: State,
summary: Summary
) =
import state.*
val apiRoutes = HttpRoutes.of[IO] {
case GET -> Root / "summary" =>
Ok(summary)

case GET -> Root / "ws" / "events" =>
val continuous =
rf.discrete
Expand Down
6 changes: 6 additions & 0 deletions modules/tracer/frontend/src/main/scala/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ object Api:
.flatMap(_.text())
.map(str => readFromStringReentrant[Vector[Message]](str))

def summary: Future[Summary] =
fetch("/api/summary")
.flatMap(_.text())
.map(str => readFromStringReentrant[Summary](str))


def rawAll: Future[Vector[RawMessage]] =
fetch("/api/raw/all")
.flatMap(_.text())
Expand Down
22 changes: 19 additions & 3 deletions modules/tracer/frontend/src/main/scala/Frontend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,27 @@ object Frontend:
val app =
div(
fontFamily := "'Wotfard',Futura,-apple-system,sans-serif",
margin := "10px",
div(
display.flex,
alignItems.center,
h1("Langoustine tracer"),
p(marginLeft := "15px", "welcome to the future of LSP tooling")
justifyContent.spaceBetween,
div(
h1(marginTop := "0px", "Langoustine tracer"),
p(
"welcome to the future of LSP tooling"
)
),
child.maybe <-- Signal.fromFuture(Api.summary).map {
_.map { summary =>
val cmd = summary.serverCommand.mkString(" ")
dom.document.title = s"Tracer: $cmd"
div(
marginLeft := "15px",
p(b("In folder: "), summary.workingFolder),
p(b("LSP command: "), cmd)
)
}
}
),
switcher,
child <-- page.signal.map {
Expand Down
22 changes: 10 additions & 12 deletions modules/tracer/frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@

import path from 'path'

export default {
// root: path.resolve(__dirname, './../target/js-3'),
server: {
proxy: {
// '/api/ws/events': {
// target: 'ws://localhost:9977/api/ws/events',
// ws: true,
// changeOrigin: true
// },
'/api': {
target: 'http://localhost:9977',
changeOrigin: true,
export default () => {
const port = process.env.LANGOUSTINE_PORT;
const str = `http://localhost:${port}`;
return {
server: {
proxy: {
'/api': {
target: str,
changeOrigin: true
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions modules/tracer/shared/src/main/scala/Protocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,7 @@ object RawMessage:
) =
RawMessage("2.0", method, result, error, params, id)
end RawMessage

case class Summary(workingFolder: String, serverCommand: List[String])
object Summary:
given JsonValueCodec[Summary] = JsonCodecMaker.make
3 changes: 3 additions & 0 deletions modules/tracer/tests/src/test/scala/Front.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ case class Front(client: Client[IO], base: Uri, ws: WSClient[IO]):
def request(callId: CallId) =
client.expect[RawMessage](base.withPath(s"/api/raw/request/${cid(callId)}"))

def summary =
client.expect[Summary](base.withPath(s"/api/summary"))

def request(callId: Option[CallId]) =
client.expect[RawMessage](
base.withPath(s"/api/raw/request/${cid(callId.get)}")
Expand Down
5 changes: 4 additions & 1 deletion modules/tracer/tests/src/test/scala/ServerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ abstract class ServerSpec extends IOSuite:
out = out.stream.unchunks,
err = err.stream.unchunks
)
.runResource(Config(9914, NonEmptyList.of("echo")))
.runResource(
Config(9914, NonEmptyList.of("echo")),
Summary(System.getProperty("user.dir"), List("echo", "world"))
)

import org.http4s.jdkhttpclient.*

Expand Down
9 changes: 9 additions & 0 deletions modules/tracer/tests/src/test/scala/TracerServerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ object TracerServerSpec extends ServerSpec:
yield expect.all(logs.containsSlice(Seq("hello", "world")))
}

test("Serves summary at /api/summary") { serv =>
serv.front.summary.map { summary =>
expect.all(
summary.workingFolder == System.getProperty("user.dir"),
summary.serverCommand == List("echo", "world")
)
}
}

test("Serves JavaScript at /assets/main.js") { serv =>
serv.front.client
.expect[String](serv.front.base.withPath("/assets/main.js"))
Expand Down

0 comments on commit 42ac8aa

Please sign in to comment.