From 4e4b181033b5e028021f08bd6b1dfd26f37e1a1e Mon Sep 17 00:00:00 2001 From: Christoffer Artmann Date: Tue, 9 Dec 2025 11:33:23 +0100 Subject: [PATCH 1/3] fix: Ensure that Code blocks are Code blocks. Adding code blocks is handled by the VS Code so we can't modify that behaviour, so instead we check that the block has the correct settings after it's created. --- .../deepnoteNewCellLanguageService.ts | 49 +++ ...eepnoteNewCellLanguageService.unit.test.ts | 323 ++++++++++++++++++ src/notebooks/serviceRegistry.node.ts | 5 + src/notebooks/serviceRegistry.web.ts | 5 + src/test/mocks/vsc/extHostedTypes.ts | 1 + 5 files changed, 383 insertions(+) create mode 100644 src/notebooks/deepnote/deepnoteNewCellLanguageService.ts create mode 100644 src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts diff --git a/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts b/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts new file mode 100644 index 0000000000..4a4d397c8c --- /dev/null +++ b/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from 'inversify'; +import { languages, NotebookCellKind, NotebookDocumentChangeEvent, workspace } from 'vscode'; + +import { DEEPNOTE_NOTEBOOK_TYPE } from '../../kernels/deepnote/types'; +import { IExtensionSyncActivationService } from '../../platform/activation/types'; +import { PYTHON_LANGUAGE } from '../../platform/common/constants'; +import { IDisposableRegistry } from '../../platform/common/types'; +import { noop } from '../../platform/common/utils/misc'; + +/** + * Ensures newly added code cells in Deepnote notebooks default to Python language. + * VS Code copies the language from adjacent cells when inserting, which causes + * new cells after SQL blocks to be SQL. This service corrects that by resetting + * unintentional language inheritance to Python. + */ +@injectable() +export class DeepnoteNewCellLanguageService implements IExtensionSyncActivationService { + constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} + + public activate(): void { + this.disposables.push(workspace.onDidChangeNotebookDocument(this.onDidChangeNotebookDocument, this)); + } + + private async onDidChangeNotebookDocument(e: NotebookDocumentChangeEvent): Promise { + if (e.notebook.notebookType !== DEEPNOTE_NOTEBOOK_TYPE) { + return; + } + + for (const change of e.contentChanges) { + for (const cell of change.addedCells) { + // Only process empty code cells + if (cell.kind !== NotebookCellKind.Code) continue; + if (cell.document.getText().trim().length > 0) continue; + + // Check if this is an intentional special block (has __deepnotePocket metadata) + const pocketType = cell.metadata?.__deepnotePocket?.type; + if (pocketType) { + // This is an intentional SQL, chart, or input block - keep its language + continue; + } + + // If the cell inherited a non-Python language, reset to Python + if (cell.document.languageId !== PYTHON_LANGUAGE) { + languages.setTextDocumentLanguage(cell.document, PYTHON_LANGUAGE).then(noop, noop); + } + } + } + } +} diff --git a/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts b/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts new file mode 100644 index 0000000000..b2fea0e2a4 --- /dev/null +++ b/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts @@ -0,0 +1,323 @@ +import { expect } from 'chai'; +import { anything, verify, when } from 'ts-mockito'; +import { Disposable, NotebookCell, NotebookCellKind, NotebookDocument, TextDocument, Uri } from 'vscode'; + +import { IDisposableRegistry } from '../../platform/common/types'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; +import { DeepnoteNewCellLanguageService } from './deepnoteNewCellLanguageService'; + +suite('DeepnoteNewCellLanguageService', () => { + let service: DeepnoteNewCellLanguageService; + let disposables: Disposable[]; + let mockDisposableRegistry: IDisposableRegistry; + let notebookChangeHandler: ((e: any) => void) | undefined; + + function createMockNotebook(notebookType: string): NotebookDocument { + return { + uri: Uri.file('/test/notebook.deepnote'), + notebookType + } as NotebookDocument; + } + + function createMockCell(options: { + kind?: NotebookCellKind; + languageId?: string; + content?: string; + metadata?: Record; + }): NotebookCell { + const { kind = NotebookCellKind.Code, languageId = 'python', content = '', metadata = {} } = options; + + return { + index: 0, + notebook: createMockNotebook('deepnote'), + kind, + document: { + uri: Uri.file('/test/notebook.deepnote#cell0'), + languageId, + getText: () => content + } as TextDocument, + metadata, + outputs: [], + executionSummary: undefined + } as unknown as NotebookCell; + } + + setup(() => { + resetVSCodeMocks(); + disposables = []; + mockDisposableRegistry = disposables as unknown as IDisposableRegistry; + + // Capture the notebook change handler when workspace.onDidChangeNotebookDocument is called + when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( + (handler, thisArg) => { + notebookChangeHandler = (e: any) => handler.call(thisArg, e); + + return { dispose: () => undefined }; + } + ); + + // Mock languages.setTextDocumentLanguage to return a resolved promise + when(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).thenReturn( + Promise.resolve({} as TextDocument) + ); + + service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); + }); + + teardown(() => { + resetVSCodeMocks(); + notebookChangeHandler = undefined; + disposables.forEach((d) => d.dispose()); + }); + + suite('activate', () => { + test('registers workspace.onDidChangeNotebookDocument listener', () => { + service.activate(); + + verify(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).once(); + }); + + test('adds disposable to registry', () => { + // Reset mocks to isolate this test + resetVSCodeMocks(); + when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( + (handler, thisArg) => { + notebookChangeHandler = (e: any) => handler.call(thisArg, e); + + return { dispose: () => undefined }; + } + ); + disposables = []; + mockDisposableRegistry = disposables as unknown as IDisposableRegistry; + service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); + + service.activate(); + + expect(disposables.length).to.be.greaterThan(0); + }); + }); + + suite('onDidChangeNotebookDocument', () => { + setup(() => { + // Reset mock verification state between tests + resetVSCodeMocks(); + + // Re-setup the mocks after reset + when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( + (handler, thisArg) => { + notebookChangeHandler = (e: any) => handler.call(thisArg, e); + + return { dispose: () => undefined }; + } + ); + when(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).thenReturn( + Promise.resolve({} as TextDocument) + ); + + disposables = []; + mockDisposableRegistry = disposables as unknown as IDisposableRegistry; + service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); + service.activate(); + expect(notebookChangeHandler).to.not.be.undefined; + }); + + test('ignores non-deepnote notebooks', async () => { + const jupyterNotebook = createMockNotebook('jupyter-notebook'); + const cell = createMockCell({ languageId: 'sql' }); + + notebookChangeHandler!({ + notebook: jupyterNotebook, + contentChanges: [{ addedCells: [cell] }] + }); + + // Allow async operations to complete + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores markdown cells', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ kind: NotebookCellKind.Markup, languageId: 'markdown' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores cells with content', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql', content: 'SELECT * FROM table' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores cells that already have Python language', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'python' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores intentional SQL blocks (with __deepnotePocket.type)', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'sql', + metadata: { __deepnotePocket: { type: 'sql' } } + }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores intentional chart blocks (with __deepnotePocket.type)', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'json', + metadata: { __deepnotePocket: { type: 'chart-vega' } } + }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores intentional input blocks (with __deepnotePocket.type)', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'plaintext', + metadata: { __deepnotePocket: { type: 'input-text' } } + }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('changes SQL cell to Python when no __deepnotePocket metadata', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + }); + + test('changes JSON cell to Python when no __deepnotePocket metadata', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'json' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + }); + + test('handles multiple added cells', async () => { + const notebook = createMockNotebook('deepnote'); + const sqlCell = createMockCell({ languageId: 'sql' }); + const pythonCell = createMockCell({ languageId: 'python' }); + const jsonCell = createMockCell({ languageId: 'json' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [sqlCell, pythonCell, jsonCell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Should change SQL and JSON, but not Python + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(sqlCell.document, 'python')).once(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(pythonCell.document, anything())).never(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(jsonCell.document, 'python')).once(); + }); + + test('handles multiple content changes', async () => { + const notebook = createMockNotebook('deepnote'); + const cell1 = createMockCell({ languageId: 'sql' }); + const cell2 = createMockCell({ languageId: 'javascript' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell1] }, { addedCells: [cell2] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell1.document, 'python')).once(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell2.document, 'python')).once(); + }); + + test('ignores content changes with no added cells', async () => { + const notebook = createMockNotebook('deepnote'); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); + + test('ignores cells with whitespace-only content', async () => { + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql', content: ' \n\t ' }); + + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Cell with only whitespace is considered empty, so it should be changed + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + }); + }); +}); diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index a23f0a1c51..8d44992a43 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -82,6 +82,7 @@ import { DeepnoteNotebookEnvironmentMapper } from '../kernels/deepnote/environme import { DeepnoteNotebookCommandListener } from './deepnote/deepnoteNotebookCommandListener'; import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnote/deepnoteInputBlockCellStatusBarProvider'; import { DeepnoteBigNumberCellStatusBarProvider } from './deepnote/deepnoteBigNumberCellStatusBarProvider'; +import { DeepnoteNewCellLanguageService } from './deepnote/deepnoteNewCellLanguageService'; import { SqlIntegrationStartupCodeProvider } from './deepnote/integrations/sqlIntegrationStartupCodeProvider'; import { DeepnoteCellCopyHandler } from './deepnote/deepnoteCellCopyHandler'; import { DeepnoteEnvironmentTreeDataProvider } from '../kernels/deepnote/environments/deepnoteEnvironmentTreeDataProvider.node'; @@ -213,6 +214,10 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea IExtensionSyncActivationService, DeepnoteBigNumberCellStatusBarProvider ); + serviceManager.addSingleton( + IExtensionSyncActivationService, + DeepnoteNewCellLanguageService + ); // Deepnote configuration services serviceManager.addSingleton(DeepnoteEnvironmentStorage, DeepnoteEnvironmentStorage); diff --git a/src/notebooks/serviceRegistry.web.ts b/src/notebooks/serviceRegistry.web.ts index 49f5077030..708e657875 100644 --- a/src/notebooks/serviceRegistry.web.ts +++ b/src/notebooks/serviceRegistry.web.ts @@ -51,6 +51,7 @@ import { } from './deepnote/integrations/types'; import { DeepnoteInputBlockCellStatusBarItemProvider } from './deepnote/deepnoteInputBlockCellStatusBarProvider'; import { DeepnoteBigNumberCellStatusBarProvider } from './deepnote/deepnoteBigNumberCellStatusBarProvider'; +import { DeepnoteNewCellLanguageService } from './deepnote/deepnoteNewCellLanguageService'; import { SqlCellStatusBarProvider } from './deepnote/sqlCellStatusBarProvider'; import { IntegrationKernelRestartHandler } from './deepnote/integrations/integrationKernelRestartHandler'; @@ -123,6 +124,10 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea IExtensionSyncActivationService, DeepnoteBigNumberCellStatusBarProvider ); + serviceManager.addSingleton( + IExtensionSyncActivationService, + DeepnoteNewCellLanguageService + ); serviceManager.addSingleton( IExtensionSyncActivationService, SqlCellStatusBarProvider diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index 962d75bd0e..4a58f1c077 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -103,6 +103,7 @@ export namespace vscMockExtHostedTypes { } export enum NotebookCellKind { Markdown = 1, + Markup = 1, // VS Code uses 'Markup' but some older code uses 'Markdown' - both have value 1 Code = 2 } export enum NotebookCellExecutionState { From ea540d31a4b062758e9ff70894d91affd3baa41e Mon Sep 17 00:00:00 2001 From: Christoffer Artmann Date: Tue, 9 Dec 2025 13:01:58 +0100 Subject: [PATCH 2/3] cleanup --- .../deepnote/deepnoteNewCellLanguageService.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts b/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts index 4a4d397c8c..b3844e494d 100644 --- a/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts +++ b/src/notebooks/deepnote/deepnoteNewCellLanguageService.ts @@ -28,18 +28,21 @@ export class DeepnoteNewCellLanguageService implements IExtensionSyncActivationS for (const change of e.contentChanges) { for (const cell of change.addedCells) { - // Only process empty code cells - if (cell.kind !== NotebookCellKind.Code) continue; - if (cell.document.getText().trim().length > 0) continue; + if (cell.kind !== NotebookCellKind.Code) { + continue; + } + + if (cell.document.getText().trim().length > 0) { + continue; + } - // Check if this is an intentional special block (has __deepnotePocket metadata) const pocketType = cell.metadata?.__deepnotePocket?.type; + if (pocketType) { - // This is an intentional SQL, chart, or input block - keep its language continue; } - // If the cell inherited a non-Python language, reset to Python + // TODO: This will have to be revisited if we add support for other languages in Deepnote. if (cell.document.languageId !== PYTHON_LANGUAGE) { languages.setTextDocumentLanguage(cell.document, PYTHON_LANGUAGE).then(noop, noop); } From 27071c500a190ec642e426e22ae7c441ffbc1baf Mon Sep 17 00:00:00 2001 From: Christoffer Artmann Date: Wed, 10 Dec 2025 11:01:42 +0100 Subject: [PATCH 3/3] clean up the test --- ...eepnoteNewCellLanguageService.unit.test.ts | 352 ++++++++---------- 1 file changed, 152 insertions(+), 200 deletions(-) diff --git a/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts b/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts index b2fea0e2a4..6293c9efa2 100644 --- a/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts +++ b/src/notebooks/deepnote/deepnoteNewCellLanguageService.unit.test.ts @@ -9,7 +9,6 @@ import { DeepnoteNewCellLanguageService } from './deepnoteNewCellLanguageService suite('DeepnoteNewCellLanguageService', () => { let service: DeepnoteNewCellLanguageService; let disposables: Disposable[]; - let mockDisposableRegistry: IDisposableRegistry; let notebookChangeHandler: ((e: any) => void) | undefined; function createMockNotebook(notebookType: string): NotebookDocument { @@ -45,9 +44,7 @@ suite('DeepnoteNewCellLanguageService', () => { setup(() => { resetVSCodeMocks(); disposables = []; - mockDisposableRegistry = disposables as unknown as IDisposableRegistry; - // Capture the notebook change handler when workspace.onDidChangeNotebookDocument is called when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( (handler, thisArg) => { notebookChangeHandler = (e: any) => handler.call(thisArg, e); @@ -56,268 +53,223 @@ suite('DeepnoteNewCellLanguageService', () => { } ); - // Mock languages.setTextDocumentLanguage to return a resolved promise when(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).thenReturn( Promise.resolve({} as TextDocument) ); - service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); + service = new DeepnoteNewCellLanguageService(disposables as unknown as IDisposableRegistry); }); teardown(() => { - resetVSCodeMocks(); notebookChangeHandler = undefined; disposables.forEach((d) => d.dispose()); }); - suite('activate', () => { - test('registers workspace.onDidChangeNotebookDocument listener', () => { - service.activate(); - - verify(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).once(); - }); - - test('adds disposable to registry', () => { - // Reset mocks to isolate this test - resetVSCodeMocks(); - when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( - (handler, thisArg) => { - notebookChangeHandler = (e: any) => handler.call(thisArg, e); - - return { dispose: () => undefined }; - } - ); - disposables = []; - mockDisposableRegistry = disposables as unknown as IDisposableRegistry; - service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); + test('activate registers onDidChangeNotebookDocument listener', () => { + service.activate(); - service.activate(); - - expect(disposables.length).to.be.greaterThan(0); - }); + verify(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).once(); }); - suite('onDidChangeNotebookDocument', () => { - setup(() => { - // Reset mock verification state between tests - resetVSCodeMocks(); - - // Re-setup the mocks after reset - when(mockedVSCodeNamespaces.workspace.onDidChangeNotebookDocument(anything(), anything())).thenCall( - (handler, thisArg) => { - notebookChangeHandler = (e: any) => handler.call(thisArg, e); - - return { dispose: () => undefined }; - } - ); - when(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).thenReturn( - Promise.resolve({} as TextDocument) - ); - - disposables = []; - mockDisposableRegistry = disposables as unknown as IDisposableRegistry; - service = new DeepnoteNewCellLanguageService(mockDisposableRegistry); - service.activate(); - expect(notebookChangeHandler).to.not.be.undefined; - }); + test('activate adds disposable to registry', () => { + service.activate(); - test('ignores non-deepnote notebooks', async () => { - const jupyterNotebook = createMockNotebook('jupyter-notebook'); - const cell = createMockCell({ languageId: 'sql' }); - - notebookChangeHandler!({ - notebook: jupyterNotebook, - contentChanges: [{ addedCells: [cell] }] - }); + expect(disposables.length).to.be.greaterThan(0); + }); - // Allow async operations to complete - await new Promise((resolve) => setTimeout(resolve, 10)); + test('ignores non-deepnote notebooks', async () => { + service.activate(); + const jupyterNotebook = createMockNotebook('jupyter-notebook'); + const cell = createMockCell({ languageId: 'sql' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + notebookChangeHandler!({ + notebook: jupyterNotebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores markdown cells', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ kind: NotebookCellKind.Markup, languageId: 'markdown' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('ignores markdown cells', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ kind: NotebookCellKind.Markup, languageId: 'markdown' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores cells with content', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ languageId: 'sql', content: 'SELECT * FROM table' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('ignores cells with content', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql', content: 'SELECT * FROM table' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores cells that already have Python language', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ languageId: 'python' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('ignores cells that already have Python language', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'python' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores intentional SQL blocks (with __deepnotePocket.type)', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ - languageId: 'sql', - metadata: { __deepnotePocket: { type: 'sql' } } - }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); - - await new Promise((resolve) => setTimeout(resolve, 10)); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + test('ignores intentional SQL blocks (with __deepnotePocket.type)', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'sql', + metadata: { __deepnotePocket: { type: 'sql' } } }); - test('ignores intentional chart blocks (with __deepnotePocket.type)', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ - languageId: 'json', - metadata: { __deepnotePocket: { type: 'chart-vega' } } - }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + await new Promise((resolve) => setTimeout(resolve, 10)); - await new Promise((resolve) => setTimeout(resolve, 10)); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + test('ignores intentional chart blocks (with __deepnotePocket.type)', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'json', + metadata: { __deepnotePocket: { type: 'chart-vega' } } }); - test('ignores intentional input blocks (with __deepnotePocket.type)', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ - languageId: 'plaintext', - metadata: { __deepnotePocket: { type: 'input-text' } } - }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + await new Promise((resolve) => setTimeout(resolve, 10)); - await new Promise((resolve) => setTimeout(resolve, 10)); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + test('ignores intentional input blocks (with __deepnotePocket.type)', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ + languageId: 'plaintext', + metadata: { __deepnotePocket: { type: 'input-text' } } }); - test('changes SQL cell to Python when no __deepnotePocket metadata', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ languageId: 'sql' }); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] + }); + await new Promise((resolve) => setTimeout(resolve, 10)); - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('changes SQL cell to Python when no __deepnotePocket metadata', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('changes JSON cell to Python when no __deepnotePocket metadata', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ languageId: 'json' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('changes JSON cell to Python when no __deepnotePocket metadata', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'json' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('handles multiple added cells', async () => { - const notebook = createMockNotebook('deepnote'); - const sqlCell = createMockCell({ languageId: 'sql' }); - const pythonCell = createMockCell({ languageId: 'python' }); - const jsonCell = createMockCell({ languageId: 'json' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [sqlCell, pythonCell, jsonCell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('handles multiple added cells', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const sqlCell = createMockCell({ languageId: 'sql' }); + const pythonCell = createMockCell({ languageId: 'python' }); + const jsonCell = createMockCell({ languageId: 'json' }); - // Should change SQL and JSON, but not Python - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(sqlCell.document, 'python')).once(); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(pythonCell.document, anything())).never(); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(jsonCell.document, 'python')).once(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [sqlCell, pythonCell, jsonCell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('handles multiple content changes', async () => { - const notebook = createMockNotebook('deepnote'); - const cell1 = createMockCell({ languageId: 'sql' }); - const cell2 = createMockCell({ languageId: 'javascript' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell1] }, { addedCells: [cell2] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(sqlCell.document, 'python')).once(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(pythonCell.document, anything())).never(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(jsonCell.document, 'python')).once(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('handles multiple content changes', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell1 = createMockCell({ languageId: 'sql' }); + const cell2 = createMockCell({ languageId: 'javascript' }); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell1.document, 'python')).once(); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell2.document, 'python')).once(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell1] }, { addedCells: [cell2] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores content changes with no added cells', async () => { - const notebook = createMockNotebook('deepnote'); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell1.document, 'python')).once(); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell2.document, 'python')).once(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('ignores content changes with no added cells', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); - test('ignores cells with whitespace-only content', async () => { - const notebook = createMockNotebook('deepnote'); - const cell = createMockCell({ languageId: 'sql', content: ' \n\t ' }); - - notebookChangeHandler!({ - notebook, - contentChanges: [{ addedCells: [cell] }] - }); + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(anything(), anything())).never(); + }); - await new Promise((resolve) => setTimeout(resolve, 10)); + test('changes cells with whitespace-only content to Python', async () => { + service.activate(); + const notebook = createMockNotebook('deepnote'); + const cell = createMockCell({ languageId: 'sql', content: ' \n\t ' }); - // Cell with only whitespace is considered empty, so it should be changed - verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); + notebookChangeHandler!({ + notebook, + contentChanges: [{ addedCells: [cell] }] }); + await new Promise((resolve) => setTimeout(resolve, 10)); + + verify(mockedVSCodeNamespaces.languages.setTextDocumentLanguage(cell.document, 'python')).once(); }); });