Skip to content

Commit 64affd8

Browse files
committed
Support cancelling worksheet evaluation
1 parent fab368d commit 64affd8

File tree

5 files changed

+102
-22
lines changed

5 files changed

+102
-22
lines changed

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

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class DottyLanguageServer extends LanguageServer
5454

5555
private[this] var rootUri: String = _
5656
private[this] var client: LanguageClient = _
57+
private[this] val worksheets: mutable.Map[URI, CompletableFuture[_]] = mutable.Map.empty
5758

5859
private[this] var myDrivers: mutable.Map[ProjectConfig, InteractiveDriver] = _
5960

@@ -227,13 +228,25 @@ class DottyLanguageServer extends LanguageServer
227228
/*thisServer.synchronized*/ {}
228229

229230
override def didSave(params: DidSaveTextDocumentParams): Unit = {
230-
thisServer.synchronized {
231-
val uri = new URI(params.getTextDocument.getUri)
232-
if (isWorksheet(uri)) {
233-
val driver = driverFor(uri)
231+
val uri = new URI(params.getTextDocument.getUri)
232+
if (isWorksheet(uri)) {
233+
if (uri.getScheme == "cancel") {
234+
val fileURI = new URI("file", uri.getUserInfo, uri.getHost, uri.getPort, uri.getPath, uri.getQuery, uri.getFragment)
235+
worksheets.get(fileURI).foreach(_.cancel(true))
236+
} else {
234237
val sendMessage = (msg: String) => client.logMessage(new MessageParams(MessageType.Info, uri + msg))
235-
evaluateWorksheet(driver, uri, sendMessage)(driver.currentCtx)
236-
sendMessage("FINISHED")
238+
worksheets.put(
239+
uri,
240+
computeAsync { cancelChecker =>
241+
try {
242+
val driver = driverFor(uri)
243+
evaluateWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
244+
} finally {
245+
worksheets.remove(uri)
246+
sendMessage("FINISHED")
247+
}
248+
}
249+
)
237250
}
238251
}
239252
}
@@ -430,14 +443,19 @@ class DottyLanguageServer extends LanguageServer
430443
/**
431444
* Evaluate the worksheet at `uri`.
432445
*
433-
* @param driver The driver for the project that contains the worksheet.
434-
* @param uri The URI of the worksheet.
435-
* @param sendMessage A mean of communicating the results of evaluation back.
446+
* @param driver The driver for the project that contains the worksheet.
447+
* @param uri The URI of the worksheet.
448+
* @param sendMessage A mean of communicating the results of evaluation back.
449+
* @param cancelChecker Token to check whether evaluation was cancelled
436450
*/
437-
private def evaluateWorksheet(driver: InteractiveDriver, uri: URI, sendMessage: String => Unit)(implicit ctx: Context): Unit = {
451+
private def evaluateWorksheet(driver: InteractiveDriver,
452+
uri: URI,
453+
sendMessage: String => Unit,
454+
cancelChecker: CancelChecker)(
455+
implicit ctx: Context): Unit = {
438456
val trees = driver.openedTrees(uri)
439457
trees.headOption.foreach { tree =>
440-
Worksheet.evaluate(tree, sendMessage)
458+
Worksheet.evaluate(tree, sendMessage, cancelChecker)
441459
}
442460
}
443461
}

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

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,27 @@ import dotty.tools.dotc.core.Flags.Synthetic
1010

1111
import dotty.tools.repl.{ReplDriver, State}
1212

13+
import org.eclipse.lsp4j.jsonrpc.CancelChecker
14+
1315
import java.io.{ByteArrayOutputStream, PrintStream}
16+
import java.util.concurrent.{Callable, CancellationException, Executors, TimeUnit}
1417

1518
object Worksheet {
1619

20+
private val executor = Executors.newFixedThreadPool(1)
21+
private final val checkCancelledDelayMs = 50
22+
1723
/**
1824
* Evaluate `tree` as a worksheet using the REPL.
1925
*
20-
* @param tree The top level object wrapping the worksheet.
21-
* @param sendMessage A mean of communicating the results of evaluation back.
26+
* @param tree The top level object wrapping the worksheet.
27+
* @param sendMessage A mean of communicating the results of evaluation back.
28+
* @param cancelChecker A token to check whether execution should be cancelled.
2229
*/
23-
def evaluate(tree: SourceTree, sendMessage: String => Unit)(implicit ctx: Context): Unit = {
30+
def evaluate(tree: SourceTree,
31+
sendMessage: String => Unit,
32+
cancelChecker: CancelChecker)(
33+
implicit ctx: Context): Unit = {
2434
tree.tree match {
2535
case td @ TypeDef(_, template: Template) =>
2636
val replOut = new ByteArrayOutputStream
@@ -32,11 +42,28 @@ object Worksheet {
3242
state
3343

3444
case (state, statement) if executed.add(bounds(statement.pos)) =>
35-
val (line, newState) = execute(repl, state, statement, tree.source)
36-
val result = new String(replOut.toByteArray())
37-
if (result.trim.nonEmpty) sendMessage(encode(result, line))
38-
replOut.reset()
39-
newState
45+
val task = executor.submit(new Callable[State] {
46+
override def call(): State = {
47+
val (line, newState) = execute(repl, state, statement, tree.source)
48+
val result = new String(replOut.toByteArray())
49+
if (result.trim.nonEmpty) sendMessage(encode(result, line))
50+
replOut.reset()
51+
newState
52+
}
53+
})
54+
55+
while (!task.isDone()) {
56+
try {
57+
cancelChecker.checkCanceled()
58+
Thread.sleep(checkCancelledDelayMs)
59+
} catch {
60+
case _: CancellationException =>
61+
task.cancel(true)
62+
}
63+
}
64+
65+
try task.get(0, TimeUnit.SECONDS)
66+
catch { case _: Throwable => state }
4067

4168
case (state, statement) =>
4269
state

vscode-dotty/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
{
4545
"command": "worksheet.evaluate",
4646
"title": "Run worksheet"
47+
},
48+
{
49+
"command": "worksheet.cancel",
50+
"title": "Cancel worksheet evaluation"
4751
}
4852
],
4953
"configurationDefaults": {

vscode-dotty/src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ function run(serverOptions: ServerOptions) {
209209
worksheet.worksheetSave(client)
210210
})
211211

212+
vscode.commands.registerCommand(worksheet.worksheetCancelEvaluationKey, () => {
213+
worksheet.cancelExecution(client)
214+
})
215+
212216
// Push the disposable to the context's subscriptions so that the
213217
// client can be deactivated on extension deactivation
214218
extensionContext.subscriptions.push(client.start());

vscode-dotty/src/worksheet.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export const worksheetEvaluateKey = "worksheet.evaluate"
2929
*/
3030
export const worksheetEvaluateAfterSaveKey = "worksheet.evaluateAfterSave"
3131

32+
/**
33+
* The command key for cancelling worksheet evaluation.
34+
*/
35+
export const worksheetCancelEvaluationKey = "worksheet.cancel"
36+
3237
/** Is this document a worksheet? */
3338
export function isWorksheet(document: vscode.TextDocument): boolean {
3439
return document.fileName.endsWith(".sc")
@@ -112,16 +117,38 @@ function _prepareWorksheet(document: vscode.TextDocument) {
112117

113118
function showWorksheetProgress(document: vscode.TextDocument) {
114119
return vscode.window.withProgress({
115-
location: vscode.ProgressLocation.Window,
116-
title: "Evaluating worksheet"
117-
}, _ => {
120+
location: vscode.ProgressLocation.Notification,
121+
title: "Evaluating worksheet",
122+
cancellable: true
123+
}, (_, token) => {
124+
token.onCancellationRequested(_ => {
125+
vscode.commands.executeCommand(worksheetCancelEvaluationKey)
126+
})
127+
118128
function isFinished() {
119129
return worksheetFinished.get(document) || false
120130
}
121131
return wait(isFinished, 500)
122132
})
123133
}
124134

135+
/**
136+
* Cancel the execution of the worksheet.
137+
*
138+
* This sends a `textDocument/didSave` message to the language server
139+
* with the scheme of the URI set to `cancel`. The language server
140+
* interprets that as a request to cancel the execution of the specified worksheet.
141+
*/
142+
export function cancelExecution(client: LanguageClient) {
143+
const editor = vscode.window.activeTextEditor
144+
if (editor) {
145+
const document = editor.document
146+
client.sendNotification("textDocument/didSave", {
147+
textDocument: { uri: document.uri.with({ scheme: "cancel" }).toString() }
148+
})
149+
}
150+
}
151+
125152
/** Wait until `cond` evaluates to true; test every `delay` ms. */
126153
function wait(cond: () => boolean, delayMs: number): Promise<boolean> {
127154
const isFinished = cond()

0 commit comments

Comments
 (0)