Skip to content

Commit 1404b3b

Browse files
committed
Use a CodeLens to run/cancel the worksheet
This makes it possible to run the worksheet without saving the document or manually running the dotty.worksheet.run command. The new UI is also less distracting since you don't get a notification pop-up every time the worksheet is being run.
1 parent 2dd964d commit 1404b3b

File tree

2 files changed

+144
-33
lines changed

2 files changed

+144
-33
lines changed

vscode-dotty/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
},
6060
{
6161
"command": "dotty.worksheet.cancel",
62-
"title": "Cancel worksheet evaluation",
62+
"title": "Cancel running worksheet",
6363
"category": "Scala"
6464
}
6565
],

vscode-dotty/src/worksheet.ts

Lines changed: 143 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as vscode from 'vscode'
2-
import { TextEdit } from 'vscode'
2+
import {
3+
CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Command,
4+
Event, EventEmitter, ProgressLocation, Range, TextDocument, TextEdit
5+
} from 'vscode'
36

47
import {
58
asWorksheetRunParams, WorksheetRunRequest, WorksheetRunParams, WorksheetRunResult,
@@ -14,11 +17,14 @@ import { Disposable } from 'vscode-jsonrpc'
1417
*/
1518
export const worksheetRunKey = "dotty.worksheet.run"
1619

17-
/** A worksheet managed by vscode */
18-
class Worksheet {
20+
/**
21+
* The command key for cancelling a running worksheet. Exposed to users as
22+
* `Cancel running worksheet`.
23+
*/
24+
export const worksheetCancelKey = "dotty.worksheet.cancel"
1925

20-
constructor(readonly document: vscode.TextDocument, readonly client: BaseLanguageClient) {
21-
}
26+
/** A worksheet managed by vscode */
27+
class Worksheet implements Disposable {
2228

2329
/** All decorations that have been added so far */
2430
private decorationTypes: vscode.TextEditorDecorationType[] = []
@@ -32,7 +38,29 @@ class Worksheet {
3238
/** The minimum margin to add so that the decoration is shown after all text. */
3339
private margin: number = 0
3440

35-
/** Remove all decorations and resets this worksheet. */
41+
private readonly _onDidStateChange: EventEmitter<void> = new EventEmitter()
42+
/** This event is fired when the worksheet starts or stops running. */
43+
readonly onDidStateChange: Event<void> = this._onDidStateChange.event
44+
45+
/**
46+
* If this is not null, this can be used to signal cancellation of the
47+
* currently running worksheet.
48+
*/
49+
private canceller?: CancellationTokenSource = undefined
50+
51+
constructor(readonly document: vscode.TextDocument, readonly client: BaseLanguageClient) {
52+
}
53+
54+
dispose() {
55+
this.reset()
56+
if (this.canceller) {
57+
this.canceller.dispose()
58+
this.canceller = undefined
59+
}
60+
this._onDidStateChange.dispose()
61+
}
62+
63+
/** Remove all decorations, and resets this worksheet. */
3664
private reset(): void {
3765
this.decorationTypes.forEach(decoration => decoration.dispose())
3866
this.insertedLines = 0
@@ -51,26 +79,58 @@ class Worksheet {
5179
return edits
5280
}
5381

82+
/** If this worksheet is currently being run, cancel the run. */
83+
cancel(): void {
84+
if (this.canceller) {
85+
this.canceller.cancel()
86+
this.canceller = undefined
87+
88+
this._onDidStateChange.fire()
89+
}
90+
}
91+
92+
/** Is this worksheet currently being run ? */
93+
isRunning(): boolean {
94+
return this.canceller != undefined
95+
}
96+
5497
/**
55-
* Run the worksheet in `document`, display a progress bar during the run.
98+
* Run the worksheet in `document`, if a previous run is in progress, it is
99+
* cancelled first.
56100
*/
57101
run(): Promise<WorksheetRunResult> {
58-
return new Promise((resolve, reject) => {
102+
this.cancel()
103+
const canceller = new CancellationTokenSource()
104+
const token = canceller.token
105+
// This ensures that isRunning() returns true.
106+
this.canceller = canceller
107+
108+
this._onDidStateChange.fire()
109+
110+
return new Promise<WorksheetRunResult>(resolve => {
59111
const textEdits = this.prepareRun()
60112
const edit = new vscode.WorkspaceEdit()
61113
edit.set(this.document.uri, textEdits)
62114
vscode.workspace.applyEdit(edit).then(editSucceeded => {
63-
if (editSucceeded) {
64-
return resolve(vscode.window.withProgress({
65-
location: vscode.ProgressLocation.Notification,
66-
title: "Run the worksheet",
67-
cancellable: true
68-
}, (_, token) => this.client.sendRequest(
115+
if (editSucceeded && !token.isCancellationRequested)
116+
resolve(vscode.window.withProgress({
117+
location: ProgressLocation.Window,
118+
title: "Running worksheet"
119+
}, () => this.client.sendRequest(
69120
WorksheetRunRequest.type, asWorksheetRunParams(this.document), token
70121
)))
71-
} else
72-
reject()
122+
else
123+
resolve({ success: false })
73124
})
125+
}).then(result => {
126+
canceller.dispose()
127+
if (this.canceller === canceller) { // If false, a new run has already started
128+
// This ensures that isRunning() returns false.
129+
this.canceller = undefined
130+
131+
this._onDidStateChange.fire()
132+
}
133+
return result
74134
})
75135
}
76136

@@ -210,13 +270,20 @@ class Worksheet {
210270
}
211271

212272
export class WorksheetProvider implements Disposable {
213-
private disposables: Disposable[] = []
214273
private worksheets: Map<vscode.TextDocument, Worksheet> = new Map()
274+
private readonly _onDidWorksheetStateChange: EventEmitter<Worksheet> = new EventEmitter()
275+
/** This event is fired when a worksheet starts or stops running. */
276+
readonly onDidWorksheetStateChange: Event<Worksheet> = this._onDidWorksheetStateChange.event
277+
278+
private disposables: Disposable[] = [ this._onDidWorksheetStateChange ]
215279

216280
constructor(
217281
readonly client: BaseLanguageClient,
218-
readonly documentSelectors: vscode.DocumentSelector[]) {
282+
readonly documentSelector: vscode.DocumentSelector) {
283+
const codeLensProvider = new WorksheetCodeLensProvider(this)
219284
this.disposables.push(
285+
codeLensProvider,
286+
vscode.languages.registerCodeLensProvider(documentSelector, codeLensProvider),
220287
vscode.workspace.onWillSaveTextDocument(event => {
221288
const worksheet = this.worksheetFor(event.document)
222289
if (worksheet) {
@@ -231,12 +298,17 @@ export class WorksheetProvider implements Disposable {
231298
}
232299
}),
233300
vscode.workspace.onDidCloseTextDocument(document => {
234-
if (this.isWorksheet(document)) {
301+
const worksheet = this.worksheetFor(document)
302+
if (worksheet) {
303+
worksheet.dispose()
235304
this.worksheets.delete(document)
236305
}
237306
}),
238307
vscode.commands.registerCommand(worksheetRunKey, () => {
239-
this.runWorksheetCommand()
308+
this.callOnActiveWorksheet(w => w.run())
309+
}),
310+
vscode.commands.registerCommand(worksheetCancelKey, () => {
311+
this.callOnActiveWorksheet(w => w.cancel())
240312
})
241313
)
242314
client.onNotification(WorksheetPublishOutputNotification.type, params => {
@@ -245,17 +317,19 @@ export class WorksheetProvider implements Disposable {
245317
}
246318

247319
dispose() {
248-
this.disposables.forEach(d => d.dispose());
249-
this.disposables = [];
320+
this.worksheets.forEach(d => d.dispose())
321+
this.worksheets.clear()
322+
this.disposables.forEach(d => d.dispose())
323+
this.disposables = []
250324
}
251325

252326
/** Is this document a worksheet? */
253327
private isWorksheet(document: vscode.TextDocument): boolean {
254-
return this.documentSelectors.some(sel => vscode.languages.match(sel, document) > 0)
328+
return vscode.languages.match(this.documentSelector, document) > 0
255329
}
256330

257331
/** If `document` is a worksheet, create a new worksheet for it, or return the existing one. */
258-
private worksheetFor(document: vscode.TextDocument): Worksheet | undefined {
332+
worksheetFor(document: vscode.TextDocument): Worksheet | undefined {
259333
if (!this.isWorksheet(document)) return
260334
else {
261335
const existing = this.worksheets.get(document)
@@ -264,20 +338,21 @@ export class WorksheetProvider implements Disposable {
264338
} else {
265339
const newWorksheet = new Worksheet(document, this.client)
266340
this.worksheets.set(document, newWorksheet)
341+
this.disposables.push(
342+
newWorksheet.onDidStateChange(() => this._onDidWorksheetStateChange.fire(newWorksheet))
343+
)
267344
return newWorksheet
268345
}
269346
}
270347
}
271348

272-
/**
273-
* The VSCode command executed when the user select `Run worksheet`.
274-
*/
275-
private runWorksheetCommand() {
276-
const editor = vscode.window.activeTextEditor
277-
if (editor) {
278-
const worksheet = this.worksheetFor(editor.document)
349+
/** If the active text editor contains a worksheet, apply `f` to it. */
350+
private callOnActiveWorksheet(f: (_: Worksheet) => void) {
351+
let document = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document
352+
if (document) {
353+
const worksheet = this.worksheetFor(document)
279354
if (worksheet) {
280-
worksheet.run()
355+
f(worksheet)
281356
}
282357
}
283358
}
@@ -302,3 +377,39 @@ export class WorksheetProvider implements Disposable {
302377
}
303378
}
304379
}
380+
381+
class WorksheetCodeLensProvider implements CodeLensProvider, Disposable {
382+
private readonly _onDidChangeCodeLenses: EventEmitter<void> = new EventEmitter()
383+
readonly onDidChangeCodeLenses: Event<void> = this._onDidChangeCodeLenses.event
384+
385+
private disposables: Disposable[] = [ this._onDidChangeCodeLenses ]
386+
387+
constructor(readonly worksheetProvider: WorksheetProvider) {
388+
this.disposables.push(
389+
worksheetProvider.onDidWorksheetStateChange(() => this._onDidChangeCodeLenses.fire())
390+
)
391+
}
392+
393+
dispose() {
394+
this.disposables.forEach(d => d.dispose())
395+
this.disposables = []
396+
}
397+
398+
private readonly runCommand: Command = {
399+
command: worksheetRunKey,
400+
title: "Run this worksheet"
401+
}
402+
403+
private readonly cancelCommand: Command = {
404+
command: worksheetCancelKey,
405+
title: "Worksheet running, click to cancel"
406+
}
407+
408+
provideCodeLenses(document: TextDocument, token: CancellationToken) {
409+
const worksheet = this.worksheetProvider.worksheetFor(document)
410+
if (worksheet) {
411+
const cmd = worksheet.isRunning() ? this.cancelCommand : this.runCommand
412+
return [ new CodeLens(new Range(0, 0, 0, 0), cmd) ]
413+
}
414+
}
415+
}

0 commit comments

Comments
 (0)