From 0f13e0b6918a7212ad96366f61a4ae350e63d1c1 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 7 Nov 2025 15:10:24 +0100 Subject: [PATCH 1/3] Add a mutex for background tasks, and allow add new cell in background --- src/index.ts | 5 +- src/notebook-commands.ts | 230 +++++++++++++++++++++++++++++---------- 2 files changed, 176 insertions(+), 59 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3d15431..9bca69a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,7 @@ const plugin: JupyterFrontEndPlugin = { ) => { console.log('JupyterLab extension jupyterlab-ai-commands is activated!'); - const commands = app.commands; + const { commands, serviceManager } = app; registerFileCommands({ commands, @@ -42,7 +42,8 @@ const plugin: JupyterFrontEndPlugin = { commands, docManager, kernelSpecManager, - notebookTracker + notebookTracker, + serviceManager }); if (settingRegistry) { diff --git a/src/notebook-commands.ts b/src/notebook-commands.ts index fad929a..b65094d 100644 --- a/src/notebook-commands.ts +++ b/src/notebook-commands.ts @@ -1,8 +1,14 @@ import { CodeCell, ICodeCellModel, MarkdownCell } from '@jupyterlab/cells'; import { IDocumentManager } from '@jupyterlab/docmanager'; -import { DocumentWidget } from '@jupyterlab/docregistry'; -import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; -import { KernelSpec } from '@jupyterlab/services'; +import { Context, DocumentWidget } from '@jupyterlab/docregistry'; +import { + INotebookModel, + INotebookTracker, + Notebook, + NotebookModelFactory, + NotebookPanel +} from '@jupyterlab/notebook'; +import { KernelSpec, ServiceManager } from '@jupyterlab/services'; import { CommandRegistry } from '@lumino/commands'; /** @@ -50,15 +56,16 @@ async function findKernelByLanguage( async function getNotebookWidget( notebookPath: string | null | undefined, docManager: IDocumentManager, - notebookTracker?: INotebookTracker + notebookTracker?: INotebookTracker, + background?: boolean ): Promise { if (notebookPath) { let widget = docManager.findWidget(notebookPath); - if (!widget) { + if (!widget && !background) { widget = docManager.openOrReveal(notebookPath); } - if (!(widget instanceof NotebookPanel)) { + if (widget && !(widget instanceof NotebookPanel)) { throw new Error(`Widget for ${notebookPath} is not a notebook panel`); } @@ -140,17 +147,18 @@ function registerCreateNotebookCommand( } /** - * Add a cell to the current notebook with optional content + * Add a cell to a notebook with optional content */ function registerAddCellCommand( commands: CommandRegistry, docManager: IDocumentManager, - notebookTracker?: INotebookTracker + notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager ): void { const command = { id: 'jupyterlab-ai-commands:add-cell', label: 'Add Cell', - caption: 'Add a cell to the current notebook with optional content', + caption: 'Add a cell to a notebook with optional content.', describedBy: { args: { notebookPath: { @@ -165,6 +173,9 @@ function registerAddCellCommand( }, position: { description: 'Position relative to current cell (above or below)' + }, + background: { + description: 'Whether to avoid opening the notebook widget' } } }, @@ -173,71 +184,135 @@ function registerAddCellCommand( notebookPath, content = null, cellType = 'code', - position = 'below' + position = 'below', + background = false } = args; - const currentWidget = await getNotebookWidget( + if (background) { + return Private.withLock(async () => { + return await addCellOperation( + notebookPath, + content, + cellType, + position, + background, + docManager, + notebookTracker, + manager + ); + }); + } + + // Sinon on exécute directement l'opération + return await addCellOperation( notebookPath, + content, + cellType, + position, + background, docManager, - notebookTracker + notebookTracker, + manager ); - if (!currentWidget) { - return { - success: false, - error: notebookPath - ? `Failed to open notebook at path: ${notebookPath}` - : 'No active notebook and no notebook path provided' - }; - } + } + }; - const notebook = currentWidget.content; - const model = notebook.model; + commands.addCommand(command.id, command); +} - if (!model) { - return { - success: false, - error: 'No notebook model available' - }; - } +async function addCellOperation( + notebookPath: string | null | undefined, + content: string | null, + cellType: string, + position: string, + background: boolean, + docManager: IDocumentManager, + notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager +) { + const currentWidget = await getNotebookWidget( + notebookPath, + docManager, + notebookTracker, + background + ); + let notebook: Notebook | undefined; + let context: Context | undefined; + let model: INotebookModel | null = null; + if (currentWidget) { + notebook = currentWidget.content; + model = notebook.model; + } else { + if (manager && notebookPath) { + const factory = new NotebookModelFactory(); + context = new Context({ manager, factory, path: notebookPath }); + await context.initialize(false); + await context.ready; + model = context.model; + } else { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + } - const shouldReplaceFirstCell = - model.cells.length === 1 && - model.cells.get(0).sharedModel.getSource().trim() === ''; + if (!model) { + return { + success: false, + error: 'No notebook model available' + }; + } - if (shouldReplaceFirstCell) { - model.sharedModel.deleteCell(0); - } + const shouldReplaceFirstCell = + model.cells.length === 1 && + model.cells.get(0).sharedModel.getSource().trim() === ''; - const newCellData = { - cell_type: cellType, - source: content || '', - metadata: cellType === 'code' ? { trusted: true } : {} - }; + if (shouldReplaceFirstCell) { + model.sharedModel.deleteCell(0); + } - model.sharedModel.addCell(newCellData); + const newCellData = { + cell_type: cellType, + source: content || '', + metadata: cellType === 'code' ? { trusted: true } : {} + }; - if (cellType === 'markdown' && content) { - const cellIndex = model.cells.length - 1; - const cellWidget = notebook.widgets[cellIndex]; - if (cellWidget && cellWidget instanceof MarkdownCell) { - await cellWidget.ready; - cellWidget.rendered = true; - } - } + model.sharedModel.addCell(newCellData); + // Render the markdown cell if the widget is opened + if (notebook && cellType === 'markdown' && content) { + const cellIndex = model.cells.length - 1; + const cellWidget = notebook.widgets[cellIndex]; + if (cellWidget && cellWidget instanceof MarkdownCell) { + await cellWidget.ready; + cellWidget.rendered = true; + } + } + + // Save the notebook and dispose of the context if there is no opened widget + if (background && !notebook) { + if (context) { + await context.save(); + context.dispose(); + } else { return { - success: true, - message: `${cellType} cell added successfully`, - content: content || '', - cellType, - position + success: false, + message: 'The context is missing to save the Notebook' }; } - }; + } - commands.addCommand(command.id, command); + return { + success: true, + message: `${cellType} cell added successfully`, + content: content || '', + cellType, + position + }; } - /** * Get information about a notebook including number of cells and active cell index */ @@ -799,6 +874,7 @@ export interface IRegisterNotebookCommandsOptions { docManager: IDocumentManager; kernelSpecManager: KernelSpec.IManager; notebookTracker?: INotebookTracker; + serviceManager?: ServiceManager.IManager; diffMode?: 'unified' | 'split'; } @@ -813,11 +889,12 @@ export function registerNotebookCommands( docManager, kernelSpecManager, notebookTracker, + serviceManager, diffMode = 'unified' } = options; registerCreateNotebookCommand(commands, docManager, kernelSpecManager); - registerAddCellCommand(commands, docManager, notebookTracker); + registerAddCellCommand(commands, docManager, notebookTracker, serviceManager); registerGetNotebookInfoCommand(commands, docManager, notebookTracker); registerGetCellInfoCommand(commands, docManager, notebookTracker); registerSetCellContentCommand( @@ -830,3 +907,42 @@ export function registerNotebookCommands( registerDeleteCellCommand(commands, docManager, notebookTracker); registerSaveNotebookCommand(commands, docManager, notebookTracker); } + +namespace Private { + // Mutex implemented with a promise chain, that ensure running the background tasks in + // in order, waiting for each others. + let _mutex: Promise = Promise.resolve(); + + export async function lock(): Promise<() => void> { + let release!: () => void; + const p = new Promise(res => { + release = res; + }); + + const previous = _mutex; + // Chain the new promise after the previous one + _mutex = previous.then(() => p); + + // Wait for the previous holder to finish + await previous; + + let released = false; + return () => { + if (released) { + return; + } + released = true; + // resolve this lock's promise to let the next waiter run + release(); + }; + } + + export async function withLock(operation: () => Promise): Promise { + const release = await lock(); + try { + return await operation(); + } finally { + release(); + } + } +} From bfbfaea6e7300bbc4e75b45f5d6d3bea62485a81 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 7 Nov 2025 17:16:02 +0100 Subject: [PATCH 2/3] use the lock only if there is no widget --- src/notebook-commands.ts | 57 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/notebook-commands.ts b/src/notebook-commands.ts index b65094d..2257c81 100644 --- a/src/notebook-commands.ts +++ b/src/notebook-commands.ts @@ -75,6 +75,20 @@ async function getNotebookWidget( } } +/** + * Helper function to get a notebook context without widget + */ +async function getNotebookContext( + manager: ServiceManager.IManager, + path: string +): Promise> { + const factory = new NotebookModelFactory(); + const context = new Context({ manager, factory, path }); + await context.initialize(false); + await context.ready; + return context; +} + /** * Create a new Jupyter notebook with a kernel for the specified programming language */ @@ -188,30 +202,34 @@ function registerAddCellCommand( background = false } = args; - if (background) { + const currentWidget = await getNotebookWidget( + notebookPath, + docManager, + notebookTracker, + background + ); + + // Use a lock if the task must be executed in the background + if (!currentWidget && background) { return Private.withLock(async () => { - return await addCellOperation( + return await addCell( + currentWidget, notebookPath, content, cellType, position, - background, - docManager, - notebookTracker, manager ); }); } - // Sinon on exécute directement l'opération - return await addCellOperation( + // Otherwise execute it in the widget. + return await addCell( + currentWidget, notebookPath, content, cellType, position, - background, - docManager, - notebookTracker, manager ); } @@ -220,22 +238,14 @@ function registerAddCellCommand( commands.addCommand(command.id, command); } -async function addCellOperation( +async function addCell( + currentWidget: NotebookPanel | null, notebookPath: string | null | undefined, content: string | null, cellType: string, position: string, - background: boolean, - docManager: IDocumentManager, - notebookTracker?: INotebookTracker, manager?: ServiceManager.IManager ) { - const currentWidget = await getNotebookWidget( - notebookPath, - docManager, - notebookTracker, - background - ); let notebook: Notebook | undefined; let context: Context | undefined; let model: INotebookModel | null = null; @@ -244,10 +254,7 @@ async function addCellOperation( model = notebook.model; } else { if (manager && notebookPath) { - const factory = new NotebookModelFactory(); - context = new Context({ manager, factory, path: notebookPath }); - await context.initialize(false); - await context.ready; + context = await getNotebookContext(manager, notebookPath); model = context.model; } else { return { @@ -293,7 +300,7 @@ async function addCellOperation( } // Save the notebook and dispose of the context if there is no opened widget - if (background && !notebook) { + if (!currentWidget) { if (context) { await context.save(); context.dispose(); From 5bc4768400ef798590e35c1a3494e8a2b2eb6a24 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 7 Nov 2025 17:26:02 +0100 Subject: [PATCH 3/3] Allow other task in background (get cell/notebook info, delete cell, set cell) --- src/notebook-commands.ts | 501 ++++++++++++++++++++++++++------------- 1 file changed, 333 insertions(+), 168 deletions(-) diff --git a/src/notebook-commands.ts b/src/notebook-commands.ts index 2257c81..903b2e3 100644 --- a/src/notebook-commands.ts +++ b/src/notebook-commands.ts @@ -320,13 +320,15 @@ async function addCell( position }; } + /** * Get information about a notebook including number of cells and active cell index */ function registerGetNotebookInfoCommand( commands: CommandRegistry, docManager: IDocumentManager, - notebookTracker?: INotebookTracker + notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager ): void { const command = { id: 'jupyterlab-ai-commands:get-notebook-info', @@ -338,28 +340,41 @@ function registerGetNotebookInfoCommand( notebookPath: { description: 'Path to the notebook file. If not provided, uses the currently active notebook' + }, + background: { + description: 'Whether to avoid opening the notebook widget' } } }, execute: async (args: any) => { - const { notebookPath } = args; + const { background, notebookPath } = args; const currentWidget = await getNotebookWidget( notebookPath, docManager, - notebookTracker + notebookTracker, + background ); - if (!currentWidget) { - return { - success: false, - error: notebookPath - ? `Failed to open notebook at path: ${notebookPath}` - : 'No active notebook and no notebook path provided' - }; - } - const notebook = currentWidget.content; - const model = notebook.model; + let notebook: Notebook | undefined; + let context: Context | undefined; + let model: INotebookModel | null = null; + if (currentWidget) { + notebook = currentWidget.content; + model = notebook.model; + } else { + if (manager && notebookPath) { + context = await getNotebookContext(manager, notebookPath); + model = context.model; + } else { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + } if (!model) { return { @@ -368,15 +383,18 @@ function registerGetNotebookInfoCommand( }; } + const notebookName = + currentWidget?.title.label ?? context?.localPath.split('/').pop(); + const path = currentWidget?.context.path ?? context?.path; const cellCount = model.cells.length; - const activeCellIndex = notebook.activeCellIndex; - const activeCell = notebook.activeCell; + const activeCellIndex = notebook?.activeCellIndex; + const activeCell = notebook?.activeCell; const activeCellType = activeCell?.model.type || 'unknown'; return { success: true, - notebookName: currentWidget.title.label, - notebookPath: currentWidget.context.path, + notebookName, + notebookPath: path, cellCount, activeCellIndex, activeCellType, @@ -394,7 +412,8 @@ function registerGetNotebookInfoCommand( function registerGetCellInfoCommand( commands: CommandRegistry, docManager: IDocumentManager, - notebookTracker?: INotebookTracker + notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager ): void { const command = { id: 'jupyterlab-ai-commands:get-cell-info', @@ -410,29 +429,41 @@ function registerGetCellInfoCommand( cellIndex: { description: 'Index of the cell to get information for (0-based). If not provided, uses the currently active cell' + }, + background: { + description: 'Whether to avoid opening the notebook widget' } } }, execute: async (args: any) => { - const { notebookPath } = args; + const { background, notebookPath } = args; let { cellIndex } = args; const currentWidget = await getNotebookWidget( notebookPath, docManager, - notebookTracker + notebookTracker, + background ); - if (!currentWidget) { - return { - success: false, - error: notebookPath - ? `Failed to open notebook at path: ${notebookPath}` - : 'No active notebook and no notebook path provided' - }; - } - const notebook = currentWidget.content; - const model = notebook.model; + let notebook: Notebook | undefined; + let model: INotebookModel | null = null; + if (currentWidget) { + notebook = currentWidget.content; + model = notebook.model; + } else { + if (manager && notebookPath) { + const context = await getNotebookContext(manager, notebookPath); + model = context.model; + } else { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + } if (!model) { return { @@ -442,7 +473,7 @@ function registerGetCellInfoCommand( } if (cellIndex === undefined || cellIndex === null) { - cellIndex = notebook.activeCellIndex; + cellIndex = notebook?.activeCellIndex || -1; } if (cellIndex < 0 || cellIndex >= model.cells.length) { @@ -486,6 +517,7 @@ function registerSetCellContentCommand( commands: CommandRegistry, docManager: IDocumentManager, notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager, diffMode: 'unified' | 'split' = 'unified' ): void { const command = { @@ -512,6 +544,9 @@ function registerSetCellContentCommand( showDiff: { description: 'Whether to show a diff view of the changes (default: true)' + }, + background: { + description: 'Whether to avoid opening the notebook widget' } } }, @@ -521,121 +556,186 @@ function registerSetCellContentCommand( cellId, cellIndex, content, + background, showDiff = true } = args; - const notebookWidget = await getNotebookWidget( + const currentWidget = await getNotebookWidget( notebookPath, docManager, - notebookTracker + notebookTracker, + background ); - if (!notebookWidget) { - return { - success: false, - error: notebookPath - ? `Failed to open notebook at path: ${notebookPath}` - : 'No active notebook and no notebook path provided' - }; + + // Use a lock if the task must be executed in the background + if (!currentWidget && background) { + return Private.withLock(async () => { + return await setCellContent( + currentWidget, + notebookPath, + content, + showDiff ?? true, + diffMode, + commands, + cellId, + cellIndex, + manager + ); + }); } - const notebook = notebookWidget.content; - const targetNotebookPath = notebookWidget.context.path; + // Otherwise execute it in the widget. + return await setCellContent( + currentWidget, + notebookPath, + content, + showDiff ?? true, + diffMode, + commands, + cellId, + cellIndex, + manager + ); + } + }; - const model = notebook.model; + commands.addCommand(command.id, command); +} - if (!model) { - return { - success: false, - error: 'No notebook model available' - }; - } +async function setCellContent( + currentWidget: NotebookPanel | null, + notebookPath: string | null | undefined, + content: string, + showDiff: boolean, + diffMode: 'unified' | 'split' = 'unified', + commands: CommandRegistry, + cellId?: string, + cellIndex?: number, + manager?: ServiceManager.IManager +) { + let notebook: Notebook | undefined; + let context: Context | undefined; + let model: INotebookModel | null = null; + if (currentWidget) { + notebook = currentWidget.content; + model = notebook.model; + } else { + if (manager && notebookPath) { + context = await getNotebookContext(manager, notebookPath); + model = context.model; + } else { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + } - let targetCellIndex: number; - if (cellId !== undefined && cellId !== null) { - targetCellIndex = -1; - for (let i = 0; i < model.cells.length; i++) { - if (model.cells.get(i).id === cellId) { - targetCellIndex = i; - break; - } - } - if (targetCellIndex === -1) { - return { - success: false, - error: `Cell with ID '${cellId}' not found in notebook` - }; - } - } else if (cellIndex !== undefined && cellIndex !== null) { - if (cellIndex < 0 || cellIndex >= model.cells.length) { - return { - success: false, - error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.` - }; - } - targetCellIndex = cellIndex; - } else { - targetCellIndex = notebook.activeCellIndex; - if (targetCellIndex === -1 || targetCellIndex >= model.cells.length) { - return { - success: false, - error: 'No active cell or invalid active cell index' - }; - } - } + const targetNotebookPath = currentWidget?.context.path ?? context?.path; - const targetCell = model.cells.get(targetCellIndex); - if (!targetCell) { - return { - success: false, - error: `Cell at index ${targetCellIndex} not found` - }; - } + if (!model) { + return { + success: false, + error: 'No notebook model available' + }; + } - const sharedModel = targetCell.sharedModel; - const previousContent = sharedModel.getSource(); - const previousCellType = targetCell.type; - const retrievedCellId = targetCell.id; - - sharedModel.setSource(content); - - const shouldShowDiff = showDiff ?? true; - if (shouldShowDiff && previousContent !== content) { - const diffCommandId = - diffMode === 'split' - ? SPLIT_DIFF_COMMAND_ID - : UNIFIED_DIFF_COMMAND_ID; - - void commands.execute(diffCommandId, { - originalSource: previousContent, - newSource: content, - cellId: retrievedCellId, - showActionButtons: true, - openDiff: true, - notebookPath: targetNotebookPath - }); + let targetCellIndex: number; + if (cellId !== undefined && cellId !== null) { + targetCellIndex = -1; + for (let i = 0; i < model.cells.length; i++) { + if (model.cells.get(i).id === cellId) { + targetCellIndex = i; + break; } + } + if (targetCellIndex === -1) { + return { + success: false, + error: `Cell with ID '${cellId}' not found in notebook` + }; + } + } else if (cellIndex !== undefined && cellIndex !== null) { + if (cellIndex < 0 || cellIndex >= model.cells.length) { + return { + success: false, + error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.` + }; + } + targetCellIndex = cellIndex; + } else { + targetCellIndex = notebook?.activeCellIndex || -1; + if (targetCellIndex === -1 || targetCellIndex >= model.cells.length) { + return { + success: false, + error: 'No active cell or invalid active cell index' + }; + } + } + + const targetCell = model.cells.get(targetCellIndex); + if (!targetCell) { + return { + success: false, + error: `Cell at index ${targetCellIndex} not found` + }; + } + + const sharedModel = targetCell.sharedModel; + const previousContent = sharedModel.getSource(); + const previousCellType = targetCell.type; + const retrievedCellId = targetCell.id; + + sharedModel.setSource(content); + + // Show diff only if there is a current widget. + const shouldShowDiff = currentWidget && showDiff; + if (shouldShowDiff && previousContent !== content) { + const diffCommandId = + diffMode === 'split' ? SPLIT_DIFF_COMMAND_ID : UNIFIED_DIFF_COMMAND_ID; + + void commands.execute(diffCommandId, { + originalSource: previousContent, + newSource: content, + cellId: retrievedCellId, + showActionButtons: true, + openDiff: true, + notebookPath: targetNotebookPath + }); + } + // Save the notebook and dispose of the context if there is no opened widget + if (!currentWidget) { + if (context) { + await context.save(); + context.dispose(); + } else { return { - success: true, - message: - cellId !== undefined && cellId !== null - ? `Cell with ID '${cellId}' content replaced successfully` - : cellIndex !== undefined && cellIndex !== null - ? `Cell ${targetCellIndex} content replaced successfully` - : 'Active cell content replaced successfully', - notebookPath: targetNotebookPath, - cellId: retrievedCellId, - cellIndex: targetCellIndex, - previousContent, - previousCellType, - newContent: content, - wasActiveCell: cellId === undefined && cellIndex === undefined, - diffShown: shouldShowDiff && previousContent !== content + success: false, + message: 'The context is missing to save the Notebook' }; } - }; + } - commands.addCommand(command.id, command); + return { + success: true, + message: + cellId !== undefined && cellId !== null + ? `Cell with ID '${cellId}' content replaced successfully` + : cellIndex !== undefined && cellIndex !== null + ? `Cell ${targetCellIndex} content replaced successfully` + : 'Active cell content replaced successfully', + notebookPath: targetNotebookPath, + cellId: retrievedCellId, + cellIndex: targetCellIndex, + previousContent, + previousCellType, + newContent: content, + wasActiveCell: cellId === undefined && cellIndex === undefined, + diffShown: shouldShowDiff && previousContent !== content + }; } /** @@ -749,7 +849,8 @@ function registerRunCellCommand( function registerDeleteCellCommand( commands: CommandRegistry, docManager: IDocumentManager, - notebookTracker?: INotebookTracker + notebookTracker?: INotebookTracker, + manager?: ServiceManager.IManager ): void { const command = { id: 'jupyterlab-ai-commands:delete-cell', @@ -763,63 +864,111 @@ function registerDeleteCellCommand( }, cellIndex: { description: 'Index of the cell to delete (0-based)' + }, + background: { + description: 'Whether to avoid opening the notebook widget' } } }, execute: async (args: any) => { - const { notebookPath, cellIndex } = args; + const { background, notebookPath, cellIndex } = args; const currentWidget = await getNotebookWidget( notebookPath, docManager, - notebookTracker + notebookTracker, + background ); - if (!currentWidget) { - return { - success: false, - error: notebookPath - ? `Failed to open notebook at path: ${notebookPath}` - : 'No active notebook and no notebook path provided' - }; + + // Use a lock if the task must be executed in the background + if (!currentWidget && background) { + return Private.withLock(async () => { + return await deleteCell( + currentWidget, + notebookPath, + cellIndex, + manager + ); + }); } - const notebook = currentWidget.content; - const model = notebook.model; + // Otherwise execute it in the widget. + return await deleteCell(currentWidget, notebookPath, cellIndex, manager); + } + }; - if (!model) { - return { - success: false, - error: 'No notebook model available' - }; - } + commands.addCommand(command.id, command); +} - if (cellIndex < 0 || cellIndex >= model.cells.length) { - return { - success: false, - error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.` - }; - } +async function deleteCell( + currentWidget: NotebookPanel | null, + notebookPath: string | null | undefined, + cellIndex: number, + manager?: ServiceManager.IManager +) { + let notebook: Notebook | undefined; + let context: Context | undefined; + let model: INotebookModel | null = null; + if (currentWidget) { + notebook = currentWidget.content; + model = notebook.model; + } else { + if (manager && notebookPath) { + context = await getNotebookContext(manager, notebookPath); + model = context.model; + } else { + return { + success: false, + error: notebookPath + ? `Failed to open notebook at path: ${notebookPath}` + : 'No active notebook and no notebook path provided' + }; + } + } - const targetCell = model.cells.get(cellIndex); - if (!targetCell) { - return { - success: false, - error: `Cell at index ${cellIndex} not found` - }; - } + if (!model) { + return { + success: false, + error: 'No notebook model available' + }; + } + + if (cellIndex < 0 || cellIndex >= model.cells.length) { + return { + success: false, + error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.` + }; + } + + const targetCell = model.cells.get(cellIndex); + if (!targetCell) { + return { + success: false, + error: `Cell at index ${cellIndex} not found` + }; + } - model.sharedModel.deleteCell(cellIndex); + model.sharedModel.deleteCell(cellIndex); + // Save the notebook and dispose of the context if there is no opened widget + if (!currentWidget) { + if (context) { + await context.save(); + context.dispose(); + } else { return { - success: true, - message: `Cell ${cellIndex} deleted successfully`, - cellIndex, - remainingCells: model.cells.length + success: false, + message: 'The context is missing to save the Notebook' }; } - }; + } - commands.addCommand(command.id, command); + return { + success: true, + message: `Cell ${cellIndex} deleted successfully`, + cellIndex, + remainingCells: model.cells.length + }; } /** @@ -902,16 +1051,32 @@ export function registerNotebookCommands( registerCreateNotebookCommand(commands, docManager, kernelSpecManager); registerAddCellCommand(commands, docManager, notebookTracker, serviceManager); - registerGetNotebookInfoCommand(commands, docManager, notebookTracker); - registerGetCellInfoCommand(commands, docManager, notebookTracker); + registerGetNotebookInfoCommand( + commands, + docManager, + notebookTracker, + serviceManager + ); + registerGetCellInfoCommand( + commands, + docManager, + notebookTracker, + serviceManager + ); registerSetCellContentCommand( commands, docManager, notebookTracker, + serviceManager, diffMode ); registerRunCellCommand(commands, docManager, notebookTracker); - registerDeleteCellCommand(commands, docManager, notebookTracker); + registerDeleteCellCommand( + commands, + docManager, + notebookTracker, + serviceManager + ); registerSaveNotebookCommand(commands, docManager, notebookTracker); }