11import * 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
47import {
58 asWorksheetRunParams , WorksheetRunRequest , WorksheetRunParams , WorksheetRunResult ,
@@ -14,11 +17,14 @@ import { Disposable } from 'vscode-jsonrpc'
1417 */
1518export 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
212272export 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