3
3
| Distributed under the terms of the Modified BSD License.
4
4
|----------------------------------------------------------------------------*/
5
5
6
- import { type ICodeCellModel , type MarkdownCell } from '@jupyterlab/cells' ;
6
+ import {
7
+ CodeCell ,
8
+ type ICodeCellModel ,
9
+ type MarkdownCell
10
+ } from '@jupyterlab/cells' ;
7
11
import { URLExt } from '@jupyterlab/coreutils' ;
8
12
import { INotebookCellExecutor } from '@jupyterlab/notebook' ;
13
+ import { OutputPrompt , Stdin } from '@jupyterlab/outputarea' ;
9
14
import { Contents , ServerConnection } from '@jupyterlab/services' ;
10
- import { nullTranslator } from '@jupyterlab/translation' ;
15
+ import * as KernelMessage from '@jupyterlab/services/lib/kernel/messages' ;
16
+ import { nullTranslator , type ITranslator } from '@jupyterlab/translation' ;
11
17
import { ICollaborativeDrive } from './tokens' ;
12
18
import { Dialog , showDialog } from '@jupyterlab/apputils' ;
19
+ import { PromiseDelegate } from '@lumino/coreutils' ;
20
+ import { Panel } from '@lumino/widgets' ;
21
+
22
+ /**
23
+ * Polling interval for accepted execution requests.
24
+ */
25
+ const MAX_POLLING_INTERVAL = 1000 ;
13
26
14
27
/**
15
28
* Notebook cell executor posting a request to the server for execution.
@@ -136,10 +149,12 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
136
149
let success = false ;
137
150
try {
138
151
// FIXME quid of deletedCells and timing record
139
- const response = await ServerConnection . makeRequest (
152
+ const response = await requestServer (
153
+ cell as CodeCell ,
140
154
apiURL ,
141
155
init ,
142
- this . _serverSettings
156
+ this . _serverSettings ,
157
+ translator
143
158
) ;
144
159
const data = await response . json ( ) ;
145
160
success = data [ 'status' ] === 'ok' ;
@@ -166,3 +181,157 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
166
181
return Promise . resolve ( true ) ;
167
182
}
168
183
}
184
+
185
+ async function requestServer (
186
+ cell : CodeCell ,
187
+ url : string ,
188
+ init : RequestInit ,
189
+ settings : ServerConnection . ISettings ,
190
+ translator ?: ITranslator ,
191
+ interval = 100
192
+ ) : Promise < Response > {
193
+ const promise = new PromiseDelegate < Response > ( ) ;
194
+ ServerConnection . makeRequest ( url , init , settings )
195
+ . then ( async response => {
196
+ if ( ! response . ok ) {
197
+ promise . reject ( await ServerConnection . ResponseError . create ( response ) ) ;
198
+ } else if ( response . status === 202 ) {
199
+ let redirectUrl = response . headers . get ( 'Location' ) || url ;
200
+
201
+ if ( ! redirectUrl . startsWith ( settings . baseUrl ) ) {
202
+ redirectUrl = URLExt . join ( settings . baseUrl , redirectUrl ) ;
203
+ }
204
+
205
+ setTimeout (
206
+ async (
207
+ cell : CodeCell ,
208
+ url : string ,
209
+ init : RequestInit ,
210
+ settings : ServerConnection . ISettings ,
211
+ translator ?: ITranslator ,
212
+ interval ?: number
213
+ ) => {
214
+ try {
215
+ const response = await requestServer (
216
+ cell ,
217
+ url ,
218
+ init ,
219
+ settings ,
220
+ translator ,
221
+ interval
222
+ ) ;
223
+ promise . resolve ( response ) ;
224
+ } catch ( error ) {
225
+ promise . reject ( error ) ;
226
+ }
227
+ } ,
228
+ interval ,
229
+ cell ,
230
+ redirectUrl ,
231
+ { method : 'GET' } ,
232
+ settings ,
233
+ translator ,
234
+ // Evanescent interval
235
+ Math . min ( MAX_POLLING_INTERVAL , interval * 2 )
236
+ ) ;
237
+ } else if ( response . status === 300 ) {
238
+ let replyUrl = response . headers . get ( 'Location' ) || '' ;
239
+
240
+ if ( ! replyUrl . startsWith ( settings . baseUrl ) ) {
241
+ replyUrl = URLExt . join ( settings . baseUrl , replyUrl ) ;
242
+ }
243
+ const { parent_header, input_request } = await response . json ( ) ;
244
+ // TODO only the client sending the snippet will be prompted for the input
245
+ // we can have a deadlock if its connection is lost.
246
+ const panel = new Panel ( ) ;
247
+ panel . addClass ( 'jp-OutputArea-child' ) ;
248
+ panel . addClass ( 'jp-OutputArea-stdin-item' ) ;
249
+
250
+ const prompt = new OutputPrompt ( ) ;
251
+ prompt . addClass ( 'jp-OutputArea-prompt' ) ;
252
+ panel . addWidget ( prompt ) ;
253
+
254
+ const input = new Stdin ( {
255
+ future : Object . freeze ( {
256
+ sendInputReply : (
257
+ content : KernelMessage . IInputReply ,
258
+ parent_header : KernelMessage . IHeader < 'input_request' >
259
+ ) => {
260
+ ServerConnection . makeRequest (
261
+ replyUrl ,
262
+ { method : 'POST' } ,
263
+ settings
264
+ ) . catch ( error => {
265
+ console . error (
266
+ `Failed to set input to ${ JSON . stringify ( content ) } .` ,
267
+ error
268
+ ) ;
269
+ } ) ;
270
+ }
271
+ } ) as any ,
272
+ parent_header,
273
+ password : input_request . password ,
274
+ prompt : input_request . prompt ,
275
+ translator
276
+ } ) ;
277
+ input . addClass ( 'jp-OutputArea-output' ) ;
278
+ panel . addWidget ( input ) ;
279
+
280
+ // Get the input node to ensure focus after updating the model upon user reply.
281
+ const inputNode = input . node . getElementsByTagName ( 'input' ) [ 0 ] ;
282
+
283
+ void input . value . then ( value => {
284
+ panel . addClass ( 'jp-OutputArea-stdin-hiding' ) ;
285
+
286
+ // FIXME this is not great as the model should not be modified on the client.
287
+ // Use stdin as the stream so it does not get combined with stdout.
288
+ // Note: because it modifies DOM it may (will) shift focus away from the input node.
289
+ cell . outputArea . model . add ( {
290
+ output_type : 'stream' ,
291
+ name : 'stdin' ,
292
+ text : value + '\n'
293
+ } ) ;
294
+ // Refocus the input node after it lost focus due to update of the model.
295
+ inputNode . focus ( ) ;
296
+
297
+ // Keep the input in view for a little while; this (along refocusing)
298
+ // ensures that we can avoid the cell editor stealing the focus, and
299
+ // leading to user inadvertently modifying editor content when executing
300
+ // consecutive commands in short succession.
301
+ window . setTimeout ( async ( ) => {
302
+ // Tack currently focused element to ensure that it remains on it
303
+ // after disposal of the panel with the old input
304
+ // (which modifies DOM and can lead to focus jump).
305
+ const focusedElement = document . activeElement ;
306
+ // Dispose the old panel with no longer needed input box.
307
+ panel . dispose ( ) ;
308
+ // Refocus the element that was focused before.
309
+ if ( focusedElement && focusedElement instanceof HTMLElement ) {
310
+ focusedElement . focus ( ) ;
311
+ }
312
+
313
+ try {
314
+ const response = await requestServer (
315
+ cell ,
316
+ url ,
317
+ init ,
318
+ settings ,
319
+ translator
320
+ ) ;
321
+ promise . resolve ( response ) ;
322
+ } catch ( error ) {
323
+ promise . reject ( error ) ;
324
+ }
325
+ } , 500 ) ;
326
+ } ) ;
327
+
328
+ cell . outputArea . layout . addWidget ( panel ) ;
329
+ } else {
330
+ promise . resolve ( response ) ;
331
+ }
332
+ } )
333
+ . catch ( reason => {
334
+ promise . reject ( new ServerConnection . NetworkError ( reason ) ) ;
335
+ } ) ;
336
+ return promise . promise ;
337
+ }
0 commit comments