33| Distributed under the terms of the Modified BSD License.
44|----------------------------------------------------------------------------*/
55
6- import { type ICodeCellModel , type MarkdownCell } from '@jupyterlab/cells' ;
6+ import {
7+ CodeCell ,
8+ type ICodeCellModel ,
9+ type MarkdownCell
10+ } from '@jupyterlab/cells' ;
711import { URLExt } from '@jupyterlab/coreutils' ;
812import { INotebookCellExecutor } from '@jupyterlab/notebook' ;
13+ import { OutputPrompt , Stdin } from '@jupyterlab/outputarea' ;
914import { 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' ;
1117import { ICollaborativeDrive } from './tokens' ;
1218import { 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 ;
1326
1427/**
1528 * Notebook cell executor posting a request to the server for execution.
@@ -136,10 +149,12 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
136149 let success = false ;
137150 try {
138151 // FIXME quid of deletedCells and timing record
139- const response = await ServerConnection . makeRequest (
152+ const response = await requestServer (
153+ cell as CodeCell ,
140154 apiURL ,
141155 init ,
142- this . _serverSettings
156+ this . _serverSettings ,
157+ translator
143158 ) ;
144159 const data = await response . json ( ) ;
145160 success = data [ 'status' ] === 'ok' ;
@@ -166,3 +181,157 @@ export class NotebookCellServerExecutor implements INotebookCellExecutor {
166181 return Promise . resolve ( true ) ;
167182 }
168183}
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