Skip to content

Commit de5c28e

Browse files
committed
Add worksheet/exec and worksheet/publishOutput
Instead of relying on `textDocument/didSave` and `window/logMessage` like we did in the past.
1 parent a3bdde1 commit de5c28e

File tree

12 files changed

+241
-221
lines changed

12 files changed

+241
-221
lines changed

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import Interactive.Include
2929
import config.Printers.interactiv
3030

3131
import languageserver.config.ProjectConfig
32-
import languageserver.worksheet.Worksheet
32+
import languageserver.worksheet.{Worksheet, WorksheetClient, WorksheetService}
3333

3434
import lsp4j.services._
3535

@@ -42,7 +42,8 @@ import lsp4j.services._
4242
* - This implementation is based on the LSP4J library: https://github.com/eclipse/lsp4j
4343
*/
4444
class DottyLanguageServer extends LanguageServer
45-
with LanguageClientAware with TextDocumentService with WorkspaceService { thisServer =>
45+
with LanguageClientAware with TextDocumentService with WorkspaceService
46+
with WorksheetService { thisServer =>
4647
import ast.tpd._
4748

4849
import DottyLanguageServer._
@@ -54,7 +55,10 @@ class DottyLanguageServer extends LanguageServer
5455

5556

5657
private[this] var rootUri: String = _
57-
private[this] var client: LanguageClient = _
58+
59+
private[this] var myClient: WorksheetClient = _
60+
def client: WorksheetClient = myClient
61+
5862
private[this] val worksheets: ConcurrentHashMap[URI, CompletableFuture[_]] = new ConcurrentHashMap()
5963

6064
private[this] var myDrivers: mutable.Map[ProjectConfig, InteractiveDriver] = _
@@ -121,7 +125,7 @@ class DottyLanguageServer extends LanguageServer
121125
}
122126

123127
override def connect(client: LanguageClient): Unit = {
124-
this.client = client
128+
myClient = client.asInstanceOf[WorksheetClient]
125129
}
126130

127131
override def exit(): Unit = {
@@ -132,7 +136,7 @@ class DottyLanguageServer extends LanguageServer
132136
CompletableFuture.completedFuture(new Object)
133137
}
134138

135-
private[this] def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] =
139+
def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] =
136140
CompletableFutures.computeAsync { cancelToken =>
137141
// We do not support any concurrent use of the compiler currently.
138142
thisServer.synchronized {
@@ -229,27 +233,7 @@ class DottyLanguageServer extends LanguageServer
229233
/*thisServer.synchronized*/ {}
230234

231235
override def didSave(params: DidSaveTextDocumentParams): Unit = {
232-
val uri = new URI(params.getTextDocument.getUri)
233-
if (isWorksheet(uri)) {
234-
if (uri.getScheme == "cancel") {
235-
val fileURI = new URI("file", uri.getUserInfo, uri.getHost, uri.getPort, uri.getPath, uri.getQuery, uri.getFragment)
236-
Option(worksheets.get(fileURI)).foreach(_.cancel(true))
237-
} else thisServer.synchronized {
238-
val sendMessage = (msg: String) => client.logMessage(new MessageParams(MessageType.Info, uri + msg))
239-
worksheets.put(
240-
uri,
241-
computeAsync { cancelChecker =>
242-
try {
243-
val driver = driverFor(uri)
244-
evaluateWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
245-
} finally {
246-
worksheets.remove(uri)
247-
sendMessage("FINISHED")
248-
}
249-
}
250-
)
251-
}
252-
}
236+
/*thisServer.synchronized*/ {}
253237
}
254238

255239
// FIXME: share code with messages.NotAMember
@@ -447,25 +431,6 @@ class DottyLanguageServer extends LanguageServer
447431
override def resolveCodeLens(params: CodeLens) = null
448432
override def resolveCompletionItem(params: CompletionItem) = null
449433
override def signatureHelp(params: TextDocumentPositionParams) = null
450-
451-
/**
452-
* Evaluate the worksheet at `uri`.
453-
*
454-
* @param driver The driver for the project that contains the worksheet.
455-
* @param uri The URI of the worksheet.
456-
* @param sendMessage A mean of communicating the results of evaluation back.
457-
* @param cancelChecker Token to check whether evaluation was cancelled
458-
*/
459-
private def evaluateWorksheet(driver: InteractiveDriver,
460-
uri: URI,
461-
sendMessage: String => Unit,
462-
cancelChecker: CancelChecker)(
463-
implicit ctx: Context): Unit = {
464-
val trees = driver.openedTrees(uri)
465-
trees.headOption.foreach { tree =>
466-
Worksheet.evaluate(tree, sendMessage, cancelChecker)
467-
}
468-
}
469434
}
470435

471436
object DottyLanguageServer {

language-server/src/dotty/tools/languageserver/Main.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import java.nio.channels._
1010
import org.eclipse.lsp4j._
1111
import org.eclipse.lsp4j.services._
1212
import org.eclipse.lsp4j.launch._
13+
import org.eclipse.lsp4j.jsonrpc.Launcher
1314

1415
/** Run the Dotty Language Server.
1516
*
@@ -65,9 +66,16 @@ object Main {
6566
val server = new DottyLanguageServer
6667

6768
println("Starting server")
68-
// For debugging JSON messages:
69-
// val launcher = LSPLauncher.createServerLauncher(server, in, out, false, new java.io.PrintWriter(System.err, true))
70-
val launcher = LSPLauncher.createServerLauncher(server, in, out)
69+
val launcher =
70+
new Launcher.Builder[worksheet.WorksheetClient]()
71+
.setLocalService(server)
72+
.setRemoteInterface(classOf[worksheet.WorksheetClient])
73+
.setInput(in)
74+
.setOutput(out)
75+
// For debugging JSON messages:
76+
// .traceMessages(new java.io.PrintWriter(System.err, true))
77+
.create();
78+
7179
val client = launcher.getRemoteProxy()
7280
server.connect(client)
7381
launcher.startListening()

language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ object Worksheet {
2222
* @param cancelChecker A token to check whether execution should be cancelled.
2323
*/
2424
def evaluate(tree: SourceTree,
25-
sendMessage: String => Unit,
25+
sendMessage: (Int, String) => Unit,
2626
cancelChecker: CancelChecker)(
2727
implicit ctx: Context): Unit = synchronized {
2828

2929
Evaluator.get(cancelChecker) match {
3030
case None =>
31-
sendMessage(encode("Couldn't start JVM.", 1))
31+
sendMessage(1, "Couldn't start JVM.")
3232
case Some(evaluator) =>
3333
tree.tree match {
3434
case td @ TypeDef(_, template: Template) =>
@@ -42,7 +42,7 @@ object Worksheet {
4242
try {
4343
cancelChecker.checkCanceled()
4444
val (line, result) = execute(evaluator, statement, tree.source)
45-
if (result.nonEmpty) sendMessage(encode(result, line))
45+
if (result.nonEmpty) sendMessage(line, result)
4646
} catch { case _: CancellationException => () }
4747

4848
case _ =>
@@ -66,9 +66,6 @@ object Worksheet {
6666
(line, evaluator.eval(source).getOrElse(""))
6767
}
6868

69-
private def encode(message: String, line: Int): String =
70-
line + ":" + message
71-
7269
private def bounds(pos: Position): (Int, Int) = (pos.start, pos.end)
7370

7471
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dotty.tools.languageserver.worksheet
2+
3+
import org.eclipse.lsp4j.services.LanguageClient
4+
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
5+
6+
/**
7+
* A `LanguageClient` that supports the `worksheet/publishOutput` notification.
8+
*
9+
* @see dotty.tools.languageserver.worksheet.WorksheetExecOutput
10+
*/
11+
trait WorksheetClient extends LanguageClient {
12+
@JsonNotification("worksheet/publishOutput")
13+
def publishOutput(output: WorksheetExecOutput): Unit
14+
}
15+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dotty.tools.languageserver.worksheet
2+
3+
import java.net.URI
4+
5+
/** The parameter for the `worksheet/exec` request. */
6+
case class WorksheetExecParams(uri: URI)
7+
8+
/** The response to a `worksheet/exec` request. */
9+
case class WorksheetExecResponse(success: Boolean)
10+
11+
/**
12+
* A notification that tells the client that a line of a worksheet
13+
* produced the specified output.
14+
*/
15+
case class WorksheetExecOutput(uri: URI, line: Int, content: String)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package dotty.tools.languageserver.worksheet
2+
3+
import dotty.tools.dotc.core.Contexts.Context
4+
import dotty.tools.dotc.interactive.InteractiveDriver
5+
import dotty.tools.languageserver.DottyLanguageServer
6+
7+
import org.eclipse.lsp4j.jsonrpc._//{CancelChecker, CompletableFutures}
8+
import org.eclipse.lsp4j.jsonrpc.services._//{JsonSegment, JsonRequest}
9+
10+
import java.net.URI
11+
import java.util.concurrent.{CompletableFuture, ConcurrentHashMap}
12+
13+
@JsonSegment("worksheet")
14+
trait WorksheetService { thisServer: DottyLanguageServer =>
15+
16+
val worksheets: ConcurrentHashMap[URI, CompletableFuture[_]] = new ConcurrentHashMap()
17+
18+
@JsonRequest
19+
def exec(params: WorksheetExecParams): CompletableFuture[WorksheetExecResponse] = thisServer.synchronized {
20+
val uri = params.uri
21+
val future =
22+
computeAsync { cancelChecker =>
23+
try {
24+
val driver = driverFor(uri)
25+
val sendMessage = (line: Int, msg: String) => client.publishOutput(WorksheetExecOutput(uri, line, msg))
26+
evaluateWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
27+
WorksheetExecResponse(success = true)
28+
} catch {
29+
case _: Throwable =>
30+
WorksheetExecResponse(success = false)
31+
} finally {
32+
worksheets.remove(uri)
33+
}
34+
}
35+
worksheets.put(uri, future)
36+
future
37+
}
38+
39+
/**
40+
* Evaluate the worksheet at `uri`.
41+
*
42+
* @param driver The driver for the project that contains the worksheet.
43+
* @param uri The URI of the worksheet.
44+
* @param sendMessage A mean of communicating the results of evaluation back.
45+
* @param cancelChecker Token to check whether evaluation was cancelled
46+
*/
47+
private def evaluateWorksheet(driver: InteractiveDriver,
48+
uri: URI,
49+
sendMessage: (Int, String) => Unit,
50+
cancelChecker: CancelChecker)(
51+
implicit ctx: Context): Unit = {
52+
val trees = driver.openedTrees(uri)
53+
trees.headOption.foreach { tree =>
54+
Worksheet.evaluate(tree, sendMessage, cancelChecker)
55+
}
56+
}
57+
}
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
11
package dotty.tools.languageserver.util.actions
22

3+
import dotty.tools.languageserver.worksheet.{WorksheetExecOutput, WorksheetExecParams, WorksheetExecResponse}
34
import dotty.tools.languageserver.util.embedded.CodeMarker
45

5-
import org.eclipse.lsp4j.{DidSaveTextDocumentParams, MessageParams, MessageType}
6+
import java.net.URI
7+
import java.util.concurrent.CompletableFuture
68

79
abstract class WorksheetAction extends Action {
810

9-
def triggerEvaluation(marker: CodeMarker): Exec[Unit] = {
10-
val file = marker.toTextDocumentIdentifier
11-
server.didSave(new DidSaveTextDocumentParams(file))
12-
}
11+
/** Get the URI of the worksheet that contains `marker`. */
12+
def getUri(marker: CodeMarker): Exec[URI] = new URI(marker.toTextDocumentIdentifier.getUri)
1313

14-
def triggerCancellation(marker: CodeMarker): Exec[Unit] = {
15-
val file = {
16-
val file = marker.toTextDocumentIdentifier
17-
file.setUri(file.getUri.replaceFirst("file:", "cancel:"))
18-
file
19-
}
20-
server.didSave(new DidSaveTextDocumentParams(file))
14+
/** Triggers the evaluation of the worksheet. */
15+
def triggerEvaluation(marker: CodeMarker): Exec[CompletableFuture[WorksheetExecResponse]] = {
16+
val uri = getUri(marker)
17+
server.exec(WorksheetExecParams(uri))
2118
}
2219

23-
def getLogs(marker: CodeMarker): Exec[List[String]] = {
24-
def matches(params: MessageParams): Boolean =
25-
params.getType == MessageType.Info && params.getMessage.startsWith(marker.file.uri)
26-
client.log.get.collect {
27-
case params: MessageParams if matches(params) =>
28-
params.getMessage.substring(marker.file.uri.length).trim
29-
}
20+
/** The output of the worksheet that contains `marker`. */
21+
def worksheetOutput(marker: CodeMarker): Exec[List[WorksheetExecOutput]] = {
22+
val uri = getUri(marker)
23+
client.worksheetOutput.get.filter(_.uri == uri)
3024
}
25+
3126
}

language-server/test/dotty/tools/languageserver/util/actions/WorksheetCancel.scala

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,20 @@ package dotty.tools.languageserver.util.actions
33
import dotty.tools.languageserver.util.PositionContext
44
import dotty.tools.languageserver.util.embedded.CodeMarker
55

6-
import org.junit.Assert.{assertEquals, fail}
6+
import org.junit.Assert.assertTrue
77

8-
class WorksheetCancel(marker: CodeMarker, afterMs: Long) extends WorksheetAction {
8+
import java.util.concurrent.TimeUnit
99

10-
private final val cancellationTimeoutMs = 10 * 1000
10+
class WorksheetCancel(marker: CodeMarker, afterMs: Long) extends WorksheetAction {
1111

1212
override def execute(): Exec[Unit] = {
13-
triggerEvaluation(marker)
13+
val futureResult = triggerEvaluation(marker)
1414
Thread.sleep(afterMs)
15-
triggerCancellation(marker)
15+
val cancelled = futureResult.cancel(true)
1616

17-
val timeAtCancellation = System.currentTimeMillis()
18-
while (!getLogs(marker).contains("FINISHED")) {
19-
if (System.currentTimeMillis() - timeAtCancellation > cancellationTimeoutMs) {
20-
fail(s"Couldn't cancel worksheet evaluation after ${cancellationTimeoutMs} ms.")
21-
}
22-
Thread.sleep(100)
23-
}
17+
assertTrue(cancelled)
2418

25-
client.log.clear()
19+
client.worksheetOutput.clear()
2620
}
2721

2822
override def show: PositionContext.PosCtx[String] =

language-server/test/dotty/tools/languageserver/util/actions/WorksheetEvaluate.scala

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,26 @@ package dotty.tools.languageserver.util.actions
33
import dotty.tools.languageserver.util.PositionContext
44
import dotty.tools.languageserver.util.embedded.CodeMarker
55

6+
import java.util.concurrent.TimeUnit
7+
68
import org.junit.Assert.{assertEquals, assertTrue, fail}
79

810
class WorksheetEvaluate(marker: CodeMarker, expected: Seq[String], strict: Boolean) extends WorksheetAction {
911

10-
private final val evaluationTimeoutMs = 30 * 1000
11-
1212
override def execute(): Exec[Unit] = {
13-
triggerEvaluation(marker)
13+
val result = triggerEvaluation(marker).get(30, TimeUnit.SECONDS)
14+
assertTrue(result.success)
1415

15-
val timeAtEvaluation = System.currentTimeMillis()
16-
while (!getLogs(marker).contains("FINISHED")) {
17-
if (System.currentTimeMillis() - timeAtEvaluation > evaluationTimeoutMs) {
18-
fail(s"Evaluation didn't finish after ${evaluationTimeoutMs} ms.")
19-
}
20-
Thread.sleep(100)
21-
}
16+
val logs = worksheetOutput(marker).map(out => s"${out.line}:${out.content}")
2217

2318
if (strict) {
24-
assertEquals(expected, getLogs(marker).init)
19+
assertEquals(expected, logs)
2520
} else {
26-
expected.zip(getLogs(marker).init).foreach {
21+
expected.zip(logs).foreach {
2722
case (expected, message) => assertTrue(s"'$message' didn't start with '$expected'", message.startsWith(expected))
2823
}
2924
}
30-
client.log.clear()
25+
client.worksheetOutput.clear()
3126
}
3227

3328
override def show: PositionContext.PosCtx[String] =

0 commit comments

Comments
 (0)