From f61e38f9b6abf80d2bee716f63dd7351ba96fa8c Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 16:01:16 +0200 Subject: [PATCH 01/20] add SQL block variable info and input for changing --- .../deepnote/sqlCellStatusBarProvider.ts | 117 +++++++++++++++--- .../sqlCellStatusBarProvider.unit.test.ts | 106 ++++++++++++++-- 2 files changed, 192 insertions(+), 31 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index c6439f2d8e..0df2c24515 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -4,10 +4,14 @@ import { NotebookCell, NotebookCellStatusBarItem, NotebookCellStatusBarItemProvider, - NotebookDocument, + NotebookEdit, ProviderResult, + WorkspaceEdit, + commands, l10n, - notebooks + notebooks, + window, + workspace } from 'vscode'; import { inject, injectable } from 'inversify'; @@ -18,7 +22,7 @@ import { IIntegrationStorage } from './integrations/types'; import { DATAFRAME_SQL_INTEGRATION_ID } from '../../platform/notebooks/deepnote/integrationTypes'; /** - * Provides status bar items for SQL cells showing the integration name + * Provides status bar items for SQL cells showing the integration name and variable name */ @injectable() export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvider, IExtensionSyncActivationService { @@ -42,6 +46,13 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid }) ); + // Register command to update SQL variable name + this.disposables.push( + commands.registerCommand('deepnote.updateSqlVariableName', async (cell: NotebookCell) => { + await this.updateVariableName(cell); + }) + ); + // Dispose our emitter with the extension this.disposables.push(this._onDidChangeCellStatusBarItems); } @@ -59,18 +70,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return undefined; } - // Get the integration ID from cell metadata - const integrationId = this.getIntegrationId(cell); - if (!integrationId) { - return undefined; - } - - // Don't show status bar for the internal DuckDB integration - if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) { - return undefined; - } - - return this.createStatusBarItem(cell.notebook, integrationId); + return this.createStatusBarItems(cell); } private getIntegrationId(cell: NotebookCell): string | undefined { @@ -86,11 +86,29 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return undefined; } - private async createStatusBarItem( - notebook: NotebookDocument, + private async createStatusBarItems(cell: NotebookCell): Promise { + const items: NotebookCellStatusBarItem[] = []; + + // Add integration status bar item if integration ID is present + const integrationId = this.getIntegrationId(cell); + if (integrationId && integrationId !== DATAFRAME_SQL_INTEGRATION_ID) { + const integrationItem = await this.createIntegrationStatusBarItem(cell, integrationId); + if (integrationItem) { + items.push(integrationItem); + } + } + + // Always add variable status bar item for SQL cells + items.push(this.createVariableStatusBarItem(cell)); + + return items; + } + + private async createIntegrationStatusBarItem( + cell: NotebookCell, integrationId: string ): Promise { - const projectId = notebook.metadata?.deepnoteProjectId; + const projectId = cell.notebook.metadata?.deepnoteProjectId; if (!projectId) { return undefined; } @@ -111,4 +129,67 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid } }; } + + private createVariableStatusBarItem(cell: NotebookCell): NotebookCellStatusBarItem { + const variableName = this.getVariableName(cell); + + return { + text: `Variable: ${variableName}`, + alignment: 1, // NotebookCellStatusBarAlignment.Left + tooltip: l10n.t('Variable name for SQL query result\nClick to change'), + command: { + title: l10n.t('Change Variable Name'), + command: 'deepnote.updateSqlVariableName', + arguments: [cell] + } + }; + } + + private getVariableName(cell: NotebookCell): string { + const metadata = cell.metadata; + if (metadata && typeof metadata === 'object') { + const variableName = (metadata as Record).deepnote_variable_name; + if (typeof variableName === 'string' && variableName) { + return variableName; + } + } + + return 'df'; + } + + private async updateVariableName(cell: NotebookCell): Promise { + const currentVariableName = this.getVariableName(cell); + + const newVariableName = await window.showInputBox({ + prompt: l10n.t('Enter variable name for SQL query result'), + value: currentVariableName, + validateInput: (value) => { + if (!value) { + return l10n.t('Variable name cannot be empty'); + } + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) { + return l10n.t('Variable name must be a valid Python identifier'); + } + return undefined; + } + }); + + if (newVariableName === undefined || newVariableName === currentVariableName) { + return; + } + + // Update cell metadata + const edit = new WorkspaceEdit(); + const updatedMetadata = { + ...cell.metadata, + deepnote_variable_name: newVariableName + }; + + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); + + await workspace.applyEdit(edit); + + // Trigger status bar update + this._onDidChangeCellStatusBarItems.fire(); + } } diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index ac7cf16386..040ba79506 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -38,25 +38,45 @@ suite('SqlCellStatusBarProvider', () => { assert.isUndefined(result); }); - test('returns undefined for SQL cells without integration ID', async () => { + test('returns only variable status bar item for SQL cells without integration ID', async () => { const cell = createMockCell('sql', {}); const result = await provider.provideCellStatusBarItems(cell, cancellationToken); - assert.isUndefined(result); + assert.isDefined(result); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 1); + + // Check variable status bar item + const variableItem = items[0]; + assert.strictEqual(variableItem.text, 'Variable: df'); + assert.strictEqual(variableItem.alignment, 1); + assert.isDefined(variableItem.command); + assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); }); - test('returns undefined for SQL cells with dataframe integration ID', async () => { + test('returns only variable status bar item for SQL cells with dataframe integration ID', async () => { const cell = createMockCell('sql', { sql_integration_id: DATAFRAME_SQL_INTEGRATION_ID }); const result = await provider.provideCellStatusBarItems(cell, cancellationToken); - assert.isUndefined(result); + assert.isDefined(result); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 1); + + // Check variable status bar item + const variableItem = items[0]; + assert.strictEqual(variableItem.text, 'Variable: df'); + assert.strictEqual(variableItem.alignment, 1); + assert.isDefined(variableItem.command); + assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); }); - test('returns status bar item for SQL cell with integration ID', async () => { + test('returns status bar items for SQL cell with integration ID', async () => { const integrationId = 'postgres-123'; const cell = createMockCell( 'sql', @@ -82,11 +102,24 @@ suite('SqlCellStatusBarProvider', () => { const result = await provider.provideCellStatusBarItems(cell, cancellationToken); assert.isDefined(result); - assert.strictEqual((result as any).text, '$(database) My Postgres DB'); - assert.strictEqual((result as any).alignment, 1); // NotebookCellStatusBarAlignment.Left - assert.isDefined((result as any).command); - assert.strictEqual((result as any).command.command, 'deepnote.manageIntegrations'); - assert.deepStrictEqual((result as any).command.arguments, [integrationId]); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 2); + + // Check integration status bar item + const integrationItem = items[0]; + assert.strictEqual(integrationItem.text, '$(database) My Postgres DB'); + assert.strictEqual(integrationItem.alignment, 1); + assert.isDefined(integrationItem.command); + assert.strictEqual(integrationItem.command.command, 'deepnote.manageIntegrations'); + assert.deepStrictEqual(integrationItem.command.arguments, [integrationId]); + + // Check variable status bar item + const variableItem = items[1]; + assert.strictEqual(variableItem.text, 'Variable: df'); + assert.strictEqual(variableItem.alignment, 1); + assert.isDefined(variableItem.command); + assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); }); test('shows "Unknown integration (configure)" when config not found', async () => { @@ -106,10 +139,14 @@ suite('SqlCellStatusBarProvider', () => { const result = await provider.provideCellStatusBarItems(cell, cancellationToken); assert.isDefined(result); - assert.strictEqual((result as any).text, '$(database) Unknown integration (configure)'); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 2); + assert.strictEqual(items[0].text, '$(database) Unknown integration (configure)'); + assert.strictEqual(items[1].text, 'Variable: df'); }); - test('returns undefined when notebook has no project ID', async () => { + test('returns only variable item when notebook has no project ID', async () => { const integrationId = 'postgres-123'; const cell = createMockCell('sql', { sql_integration_id: integrationId @@ -117,7 +154,50 @@ suite('SqlCellStatusBarProvider', () => { const result = await provider.provideCellStatusBarItems(cell, cancellationToken); - assert.isUndefined(result); + assert.isDefined(result); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 1); + + // Check variable status bar item is still shown + const variableItem = items[0]; + assert.strictEqual(variableItem.text, 'Variable: df'); + }); + + test('shows custom variable name when set in metadata', async () => { + const integrationId = 'postgres-123'; + const cell = createMockCell( + 'sql', + { + sql_integration_id: integrationId, + deepnote_variable_name: 'my_results' + }, + { + deepnoteProjectId: 'project-1' + } + ); + + when(integrationStorage.getProjectIntegrationConfig(anything(), anything())).thenResolve({ + id: integrationId, + name: 'My Postgres DB', + type: IntegrationType.Postgres, + host: 'localhost', + port: 5432, + database: 'test', + username: 'user', + password: 'pass' + }); + + const result = await provider.provideCellStatusBarItems(cell, cancellationToken); + + assert.isDefined(result); + assert.isArray(result); + const items = result as any[]; + assert.strictEqual(items.length, 2); + + // Check variable status bar item shows custom name + const variableItem = items[1]; + assert.strictEqual(variableItem.text, 'Variable: my_results'); }); function createMockCell( From bc5b9ef3d911b521a07d3a00ad815504fff9b3b7 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 16:08:47 +0200 Subject: [PATCH 02/20] show duckdb integration --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 11 ++++++++++- .../deepnote/sqlCellStatusBarProvider.unit.test.ts | 13 ++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 0df2c24515..11e6666842 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -91,7 +91,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Add integration status bar item if integration ID is present const integrationId = this.getIntegrationId(cell); - if (integrationId && integrationId !== DATAFRAME_SQL_INTEGRATION_ID) { + if (integrationId) { const integrationItem = await this.createIntegrationStatusBarItem(cell, integrationId); if (integrationItem) { items.push(integrationItem); @@ -108,6 +108,15 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid cell: NotebookCell, integrationId: string ): Promise { + // Handle internal DuckDB integration specially + if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) { + return { + text: `$(database) ${l10n.t('DataFrame SQL (DuckDB)')}`, + alignment: 1, // NotebookCellStatusBarAlignment.Left + tooltip: l10n.t('Internal DuckDB integration for querying DataFrames') + }; + } + const projectId = cell.notebook.metadata?.deepnoteProjectId; if (!projectId) { return undefined; diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 040ba79506..31032d9631 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -56,7 +56,7 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); }); - test('returns only variable status bar item for SQL cells with dataframe integration ID', async () => { + test('returns status bar items for SQL cells with dataframe integration ID', async () => { const cell = createMockCell('sql', { sql_integration_id: DATAFRAME_SQL_INTEGRATION_ID }); @@ -66,10 +66,17 @@ suite('SqlCellStatusBarProvider', () => { assert.isDefined(result); assert.isArray(result); const items = result as any[]; - assert.strictEqual(items.length, 1); + assert.strictEqual(items.length, 2); + + // Check integration status bar item + const integrationItem = items[0]; + assert.strictEqual(integrationItem.text, '$(database) DataFrame SQL (DuckDB)'); + assert.strictEqual(integrationItem.alignment, 1); + assert.strictEqual(integrationItem.tooltip, 'Internal DuckDB integration for querying DataFrames'); + assert.isUndefined(integrationItem.command); // Check variable status bar item - const variableItem = items[0]; + const variableItem = items[1]; assert.strictEqual(variableItem.text, 'Variable: df'); assert.strictEqual(variableItem.alignment, 1); assert.isDefined(variableItem.command); From 430fec20f679beabf3a2a037e8932fce584f80f5 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 16:26:33 +0200 Subject: [PATCH 03/20] allow switching between integrations on SQL block --- .../deepnote/sqlCellStatusBarProvider.ts | 132 ++++++++++++++++-- .../sqlCellStatusBarProvider.unit.test.ts | 12 +- 2 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 11e6666842..1a261745cf 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -6,6 +6,8 @@ import { NotebookCellStatusBarItemProvider, NotebookEdit, ProviderResult, + QuickPickItem, + QuickPickItemKind, WorkspaceEdit, commands, l10n, @@ -17,9 +19,9 @@ import { inject, injectable } from 'inversify'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; import { IDisposableRegistry } from '../../platform/common/types'; -import { Commands } from '../../platform/common/constants'; import { IIntegrationStorage } from './integrations/types'; -import { DATAFRAME_SQL_INTEGRATION_ID } from '../../platform/notebooks/deepnote/integrationTypes'; +import { Commands } from '../../platform/common/constants'; +import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; /** * Provides status bar items for SQL cells showing the integration name and variable name @@ -53,6 +55,13 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid }) ); + // Register command to switch SQL integration + this.disposables.push( + commands.registerCommand('deepnote.switchSqlIntegration', async (cell: NotebookCell) => { + await this.switchIntegration(cell); + }) + ); + // Dispose our emitter with the extension this.disposables.push(this._onDidChangeCellStatusBarItems); } @@ -113,7 +122,12 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return { text: `$(database) ${l10n.t('DataFrame SQL (DuckDB)')}`, alignment: 1, // NotebookCellStatusBarAlignment.Left - tooltip: l10n.t('Internal DuckDB integration for querying DataFrames') + tooltip: l10n.t('Internal DuckDB integration for querying DataFrames\nClick to switch'), + command: { + title: l10n.t('Switch Integration'), + command: 'deepnote.switchSqlIntegration', + arguments: [cell] + } }; } @@ -126,15 +140,15 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid const config = await this.integrationStorage.getProjectIntegrationConfig(projectId, integrationId); const displayName = config?.name || l10n.t('Unknown integration (configure)'); - // Create a status bar item that opens the integration management UI + // Create a status bar item that opens the integration picker return { text: `$(database) ${displayName}`, alignment: 1, // NotebookCellStatusBarAlignment.Left - tooltip: l10n.t('SQL Integration: {0}\nClick to configure', displayName), + tooltip: l10n.t('SQL Integration: {0}\nClick to switch or configure', displayName), command: { - title: l10n.t('Configure Integration'), - command: Commands.ManageIntegrations, - arguments: [integrationId] + title: l10n.t('Switch Integration'), + command: 'deepnote.switchSqlIntegration', + arguments: [cell] } }; } @@ -201,4 +215,106 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Trigger status bar update this._onDidChangeCellStatusBarItems.fire(); } + + private async switchIntegration(cell: NotebookCell): Promise { + const currentIntegrationId = this.getIntegrationId(cell); + + // Get all available integrations + const allIntegrations = await this.integrationStorage.getAll(); + + // Build quick pick items + const items: QuickPickItem[] = []; + + // Check if current integration is unknown (not in the list) + const isCurrentIntegrationUnknown = + currentIntegrationId && + currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID && + !allIntegrations.some((i) => i.id === currentIntegrationId); + + // Add current unknown integration first if it exists + if (isCurrentIntegrationUnknown && currentIntegrationId) { + items.push({ + label: l10n.t('Unknown integration (configure)'), + description: currentIntegrationId, + detail: l10n.t('Currently selected'), + id: currentIntegrationId + } as QuickPickItem & { id: string }); + } + + // Add all configured integrations + for (const integration of allIntegrations) { + const typeLabel = this.getIntegrationTypeLabel(integration.type); + items.push({ + label: integration.name || integration.id, + description: typeLabel, + detail: integration.id === currentIntegrationId ? l10n.t('Currently selected') : undefined, + // Store the integration ID in a custom property + id: integration.id + } as QuickPickItem & { id: string }); + } + + // Add DuckDB integration + items.push({ + label: l10n.t('DataFrame SQL (DuckDB)'), + description: l10n.t('DuckDB'), + detail: currentIntegrationId === DATAFRAME_SQL_INTEGRATION_ID ? l10n.t('Currently selected') : undefined, + id: DATAFRAME_SQL_INTEGRATION_ID + } as QuickPickItem & { id: string }); + + // Add separator + items.push({ + label: '', + kind: QuickPickItemKind.Separator + }); + + // Add "Configure current integration" option + if (currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID) { + items.push({ + label: l10n.t('Configure current integration'), + id: '__configure__' + } as QuickPickItem & { id: string }); + } + + const selected = await window.showQuickPick(items, { + placeHolder: l10n.t('Select SQL integration'), + matchOnDescription: true + }); + + if (!selected) { + return; + } + + const selectedId = (selected as QuickPickItem & { id: string }).id; + + // Handle "Configure current integration" option + if (selectedId === '__configure__' && currentIntegrationId) { + await commands.executeCommand(Commands.ManageIntegrations, currentIntegrationId); + return; + } + + // Update cell metadata with new integration ID + const edit = new WorkspaceEdit(); + const updatedMetadata = { + ...cell.metadata, + sql_integration_id: selectedId + }; + + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); + + await workspace.applyEdit(edit); + + // Trigger status bar update + this._onDidChangeCellStatusBarItems.fire(); + } + + private getIntegrationTypeLabel(type: IntegrationType): string { + switch (type) { + case IntegrationType.Postgres: + return l10n.t('PostgreSQL'); + case IntegrationType.BigQuery: + return l10n.t('BigQuery'); + default: + return type; + } + } } diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 31032d9631..c8077b1a1c 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -72,8 +72,12 @@ suite('SqlCellStatusBarProvider', () => { const integrationItem = items[0]; assert.strictEqual(integrationItem.text, '$(database) DataFrame SQL (DuckDB)'); assert.strictEqual(integrationItem.alignment, 1); - assert.strictEqual(integrationItem.tooltip, 'Internal DuckDB integration for querying DataFrames'); - assert.isUndefined(integrationItem.command); + assert.strictEqual( + integrationItem.tooltip, + 'Internal DuckDB integration for querying DataFrames\nClick to switch' + ); + assert.isDefined(integrationItem.command); + assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); // Check variable status bar item const variableItem = items[1]; @@ -118,8 +122,8 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(integrationItem.text, '$(database) My Postgres DB'); assert.strictEqual(integrationItem.alignment, 1); assert.isDefined(integrationItem.command); - assert.strictEqual(integrationItem.command.command, 'deepnote.manageIntegrations'); - assert.deepStrictEqual(integrationItem.command.arguments, [integrationId]); + assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); + assert.deepStrictEqual(integrationItem.command.arguments, [cell]); // Check variable status bar item const variableItem = items[1]; From 3fa65adaadee111c4bda69aa5c85449ceecc4a59 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 16:33:25 +0200 Subject: [PATCH 04/20] show "no integration" when sql block is not connected --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 14 +++++++++++++- .../deepnote/sqlCellStatusBarProvider.unit.test.ts | 13 ++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 1a261745cf..dad897001a 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -98,13 +98,25 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid private async createStatusBarItems(cell: NotebookCell): Promise { const items: NotebookCellStatusBarItem[] = []; - // Add integration status bar item if integration ID is present + // Add integration status bar item const integrationId = this.getIntegrationId(cell); if (integrationId) { const integrationItem = await this.createIntegrationStatusBarItem(cell, integrationId); if (integrationItem) { items.push(integrationItem); } + } else { + // Show "No integration connected" when no integration is selected + items.push({ + text: `$(database) ${l10n.t('No integration connected')}`, + alignment: 1, // NotebookCellStatusBarAlignment.Left + tooltip: l10n.t('No SQL integration connected\nClick to select an integration'), + command: { + title: l10n.t('Switch Integration'), + command: 'deepnote.switchSqlIntegration', + arguments: [cell] + } + }); } // Always add variable status bar item for SQL cells diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index c8077b1a1c..5446ffd71d 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -38,7 +38,7 @@ suite('SqlCellStatusBarProvider', () => { assert.isUndefined(result); }); - test('returns only variable status bar item for SQL cells without integration ID', async () => { + test('returns status bar items for SQL cells without integration ID', async () => { const cell = createMockCell('sql', {}); const result = await provider.provideCellStatusBarItems(cell, cancellationToken); @@ -46,10 +46,17 @@ suite('SqlCellStatusBarProvider', () => { assert.isDefined(result); assert.isArray(result); const items = result as any[]; - assert.strictEqual(items.length, 1); + assert.strictEqual(items.length, 2); + + // Check "No integration connected" status bar item + const integrationItem = items[0]; + assert.strictEqual(integrationItem.text, '$(database) No integration connected'); + assert.strictEqual(integrationItem.alignment, 1); + assert.isDefined(integrationItem.command); + assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); // Check variable status bar item - const variableItem = items[0]; + const variableItem = items[1]; assert.strictEqual(variableItem.text, 'Variable: df'); assert.strictEqual(variableItem.alignment, 1); assert.isDefined(variableItem.command); From 0f40641e02fbf00714ae593ef8b29e9722395626 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 17:10:46 +0200 Subject: [PATCH 05/20] localize variable input label --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index dad897001a..649330b886 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -169,7 +169,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid const variableName = this.getVariableName(cell); return { - text: `Variable: ${variableName}`, + text: l10n.t('Variable: {0}', variableName), alignment: 1, // NotebookCellStatusBarAlignment.Left tooltip: l10n.t('Variable name for SQL query result\nClick to change'), command: { From 3be7bd11c8b4779ab19c5c1b04043c10f0b67d82 Mon Sep 17 00:00:00 2001 From: jankuca Date: Mon, 20 Oct 2025 17:27:44 +0200 Subject: [PATCH 06/20] add check for failed block update --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 649330b886..2fb2fb0943 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -222,7 +222,11 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); - await workspace.applyEdit(edit); + const success = await workspace.applyEdit(edit); + if (!success) { + void window.showErrorMessage(l10n.t('Failed to update variable name')); + return; + } // Trigger status bar update this._onDidChangeCellStatusBarItems.fire(); @@ -313,7 +317,11 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, updatedMetadata)]); - await workspace.applyEdit(edit); + const success = await workspace.applyEdit(edit); + if (!success) { + void window.showErrorMessage(l10n.t('Failed to select integration')); + return; + } // Trigger status bar update this._onDidChangeCellStatusBarItems.fire(); From a54ad0c6ef240fecc88f4b0baa65fb3d13b39487 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:03:28 +0200 Subject: [PATCH 07/20] test: add tests for sql cell status bar actions --- .../sqlCellStatusBarProvider.unit.test.ts | 421 +++++++++++++++++- 1 file changed, 420 insertions(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 5446ffd71d..6a0a6a6396 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -1,8 +1,9 @@ import { assert } from 'chai'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import { CancellationToken, CancellationTokenSource, + EventEmitter, NotebookCell, NotebookCellKind, NotebookDocument, @@ -14,6 +15,9 @@ import { IDisposableRegistry } from '../../platform/common/types'; import { IIntegrationStorage } from './integrations/types'; import { SqlCellStatusBarProvider } from './sqlCellStatusBarProvider'; import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock'; +import { createEventHandler } from '../../test/common'; +import { Commands } from '../../platform/common/constants'; suite('SqlCellStatusBarProvider', () => { let provider: SqlCellStatusBarProvider; @@ -218,6 +222,421 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.text, 'Variable: my_results'); }); + suite('activate', () => { + let activateDisposables: IDisposableRegistry; + let activateProvider: SqlCellStatusBarProvider; + let activateIntegrationStorage: IIntegrationStorage; + + setup(() => { + resetVSCodeMocks(); + activateDisposables = []; + activateIntegrationStorage = mock(); + activateProvider = new SqlCellStatusBarProvider(activateDisposables, instance(activateIntegrationStorage)); + }); + + teardown(() => { + resetVSCodeMocks(); + }); + + test('registers notebook cell status bar provider for deepnote notebooks', () => { + activateProvider.activate(); + + verify( + mockedVSCodeNamespaces.notebooks.registerNotebookCellStatusBarItemProvider('deepnote', activateProvider) + ).once(); + }); + + test('registers deepnote.updateSqlVariableName command', () => { + activateProvider.activate(); + + verify( + mockedVSCodeNamespaces.commands.registerCommand('deepnote.updateSqlVariableName', anything()) + ).once(); + }); + + test('registers deepnote.switchSqlIntegration command', () => { + activateProvider.activate(); + + verify(mockedVSCodeNamespaces.commands.registerCommand('deepnote.switchSqlIntegration', anything())).once(); + }); + + test('adds all registrations to disposables', () => { + activateProvider.activate(); + + // Should have 5 disposables: + // 1. notebook cell status bar provider + // 2. integration storage change listener + // 3. updateSqlVariableName command + // 4. switchSqlIntegration command + // 5. event emitter + assert.strictEqual(activateDisposables.length, 5); + }); + + test('listens to integration storage changes', () => { + const onDidChangeIntegrations = new EventEmitter(); + when(activateIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + + activateProvider.activate(); + + // Verify the listener was registered by checking disposables + assert.isTrue(activateDisposables.length > 0); + }); + }); + + suite('event listeners', () => { + let eventDisposables: IDisposableRegistry; + let eventProvider: SqlCellStatusBarProvider; + let eventIntegrationStorage: IIntegrationStorage; + + setup(() => { + eventDisposables = []; + eventIntegrationStorage = mock(); + eventProvider = new SqlCellStatusBarProvider(eventDisposables, instance(eventIntegrationStorage)); + }); + + test('fires onDidChangeCellStatusBarItems when integration storage changes', () => { + const onDidChangeIntegrations = new EventEmitter(); + when(eventIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + + eventProvider.activate(); + + const statusBarChangeHandler = createEventHandler( + eventProvider, + 'onDidChangeCellStatusBarItems', + eventDisposables + ); + + // Fire integration storage change event + onDidChangeIntegrations.fire(); + + assert.strictEqual(statusBarChangeHandler.count, 1, 'onDidChangeCellStatusBarItems should fire once'); + }); + + test('fires onDidChangeCellStatusBarItems multiple times for multiple integration changes', () => { + const onDidChangeIntegrations = new EventEmitter(); + when(eventIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); + + eventProvider.activate(); + + const statusBarChangeHandler = createEventHandler( + eventProvider, + 'onDidChangeCellStatusBarItems', + eventDisposables + ); + + // Fire integration storage change event multiple times + onDidChangeIntegrations.fire(); + onDidChangeIntegrations.fire(); + onDidChangeIntegrations.fire(); + + assert.strictEqual( + statusBarChangeHandler.count, + 3, + 'onDidChangeCellStatusBarItems should fire three times' + ); + }); + }); + + suite('updateSqlVariableName command handler', () => { + let commandDisposables: IDisposableRegistry; + let commandProvider: SqlCellStatusBarProvider; + let commandIntegrationStorage: IIntegrationStorage; + let updateVariableNameHandler: Function; + + setup(() => { + resetVSCodeMocks(); + commandDisposables = []; + commandIntegrationStorage = mock(); + commandProvider = new SqlCellStatusBarProvider(commandDisposables, instance(commandIntegrationStorage)); + + // Capture the command handler + when( + mockedVSCodeNamespaces.commands.registerCommand('deepnote.updateSqlVariableName', anything()) + ).thenCall((_, handler) => { + updateVariableNameHandler = handler; + return { + dispose: () => { + return; + } + }; + }); + + commandProvider.activate(); + }); + + teardown(() => { + resetVSCodeMocks(); + }); + + test('updates cell metadata with new variable name', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'old_name' }); + const newVariableName = 'new_name'; + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(newVariableName)); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await updateVariableNameHandler(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('does not update if user cancels input box', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'old_name' }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(undefined)); + + await updateVariableNameHandler(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('does not update if new name is same as current name', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'same_name' }); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve('same_name')); + + await updateVariableNameHandler(cell); + + verify(mockedVSCodeNamespaces.window.showInputBox(anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('shows error message if workspace edit fails', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'old_name' }); + const newVariableName = 'new_name'; + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(newVariableName)); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(false)); + + await updateVariableNameHandler(cell); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + }); + + test('fires onDidChangeCellStatusBarItems after successful update', async () => { + const cell = createMockCell('sql', { deepnote_variable_name: 'old_name' }); + const newVariableName = 'new_name'; + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenReturn(Promise.resolve(newVariableName)); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + const statusBarChangeHandler = createEventHandler( + commandProvider, + 'onDidChangeCellStatusBarItems', + commandDisposables + ); + + await updateVariableNameHandler(cell); + + assert.strictEqual(statusBarChangeHandler.count, 1, 'onDidChangeCellStatusBarItems should fire once'); + }); + + test('validates input - rejects empty variable name', async () => { + const cell = createMockCell('sql', {}); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenCall((options) => { + const validationResult = options.validateInput(''); + assert.strictEqual(validationResult, 'Variable name cannot be empty'); + return Promise.resolve(undefined); + }); + + await updateVariableNameHandler(cell); + }); + + test('validates input - rejects invalid Python identifier', async () => { + const cell = createMockCell('sql', {}); + + when(mockedVSCodeNamespaces.window.showInputBox(anything())).thenCall((options) => { + const validationResult = options.validateInput('123invalid'); + assert.strictEqual(validationResult, 'Variable name must be a valid Python identifier'); + return Promise.resolve(undefined); + }); + + await updateVariableNameHandler(cell); + }); + }); + + suite('switchSqlIntegration command handler', () => { + let commandDisposables: IDisposableRegistry; + let commandProvider: SqlCellStatusBarProvider; + let commandIntegrationStorage: IIntegrationStorage; + let switchIntegrationHandler: Function; + + setup(() => { + resetVSCodeMocks(); + commandDisposables = []; + commandIntegrationStorage = mock(); + commandProvider = new SqlCellStatusBarProvider(commandDisposables, instance(commandIntegrationStorage)); + + // Capture the command handler + when(mockedVSCodeNamespaces.commands.registerCommand('deepnote.switchSqlIntegration', anything())).thenCall( + (_, handler) => { + switchIntegrationHandler = handler; + return { + dispose: () => { + return; + } + }; + } + ); + + commandProvider.activate(); + }); + + teardown(() => { + resetVSCodeMocks(); + }); + + test('updates cell metadata with selected integration', async () => { + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const newIntegrationId = 'new-integration'; + + when(commandIntegrationStorage.getAll()).thenReturn( + Promise.resolve([ + { + id: newIntegrationId, + name: 'New Integration', + type: IntegrationType.Postgres, + host: 'localhost', + port: 5432, + database: 'test', + username: 'user', + password: 'pass' + } + ]) + ); + + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) + ); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + await switchIntegrationHandler(cell); + + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).once(); + }); + + test('does not update if user cancels quick pick', async () => { + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + + when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve(undefined) + ); + + await switchIntegrationHandler(cell); + + verify(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('shows error message if workspace edit fails', async () => { + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const newIntegrationId = 'new-integration'; + + when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) + ); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(false)); + + await switchIntegrationHandler(cell); + + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).once(); + }); + + test('fires onDidChangeCellStatusBarItems after successful update', async () => { + const cell = createMockCell('sql', { sql_integration_id: 'old-integration' }); + const newIntegrationId = 'new-integration'; + + when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ id: newIntegrationId, label: 'New Integration' } as any) + ); + when(mockedVSCodeNamespaces.workspace.applyEdit(anything())).thenReturn(Promise.resolve(true)); + + const statusBarChangeHandler = createEventHandler( + commandProvider, + 'onDidChangeCellStatusBarItems', + commandDisposables + ); + + await switchIntegrationHandler(cell); + + assert.strictEqual(statusBarChangeHandler.count, 1, 'onDidChangeCellStatusBarItems should fire once'); + }); + + test('executes manage integrations command when configure option is selected', async () => { + const cell = createMockCell('sql', { sql_integration_id: 'current-integration' }); + + when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenReturn( + Promise.resolve({ id: '__configure__', label: 'Configure current integration' } as any) + ); + when(mockedVSCodeNamespaces.commands.executeCommand(anything(), anything())).thenReturn( + Promise.resolve(undefined) + ); + + await switchIntegrationHandler(cell); + + verify( + mockedVSCodeNamespaces.commands.executeCommand(Commands.ManageIntegrations, 'current-integration') + ).once(); + verify(mockedVSCodeNamespaces.workspace.applyEdit(anything())).never(); + }); + + test('includes DuckDB integration in quick pick items', async () => { + const cell = createMockCell('sql', {}); + let quickPickItems: any[] = []; + + when(commandIntegrationStorage.getAll()).thenReturn(Promise.resolve([])); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenCall((items) => { + quickPickItems = items; + return Promise.resolve(undefined); + }); + + await switchIntegrationHandler(cell); + + const duckDbItem = quickPickItems.find((item) => item.id === DATAFRAME_SQL_INTEGRATION_ID); + assert.isDefined(duckDbItem, 'DuckDB integration should be in quick pick items'); + assert.strictEqual(duckDbItem.label, 'DataFrame SQL (DuckDB)'); + }); + + test('marks current integration as selected in quick pick', async () => { + const currentIntegrationId = 'current-integration'; + const cell = createMockCell('sql', { sql_integration_id: currentIntegrationId }); + let quickPickItems: any[] = []; + + when(commandIntegrationStorage.getAll()).thenReturn( + Promise.resolve([ + { + id: currentIntegrationId, + name: 'Current Integration', + type: IntegrationType.Postgres, + host: 'localhost', + port: 5432, + database: 'test', + username: 'user', + password: 'pass' + } + ]) + ); + when(mockedVSCodeNamespaces.window.showQuickPick(anything(), anything())).thenCall((items) => { + quickPickItems = items; + return Promise.resolve(undefined); + }); + + await switchIntegrationHandler(cell); + + const currentItem = quickPickItems.find((item) => item.id === currentIntegrationId); + assert.isDefined(currentItem, 'Current integration should be in quick pick items'); + assert.strictEqual(currentItem.detail, 'Currently selected'); + }); + }); + function createMockCell( languageId: string, cellMetadata: Record, From 087189d105dc17cc346ea7ec56a1a303164449b9 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:19:58 +0200 Subject: [PATCH 08/20] add handling for no cell being selected --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 2fb2fb0943..9c80417368 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -50,14 +50,22 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Register command to update SQL variable name this.disposables.push( - commands.registerCommand('deepnote.updateSqlVariableName', async (cell: NotebookCell) => { + commands.registerCommand('deepnote.updateSqlVariableName', async (cell?: NotebookCell) => { + if (!cell) { + void window.showErrorMessage(l10n.t('No active notebook cell')); + return; + } await this.updateVariableName(cell); }) ); // Register command to switch SQL integration this.disposables.push( - commands.registerCommand('deepnote.switchSqlIntegration', async (cell: NotebookCell) => { + commands.registerCommand('deepnote.switchSqlIntegration', async (cell?: NotebookCell) => { + if (!cell) { + void window.showErrorMessage(l10n.t('No active notebook cell')); + return; + } await this.switchIntegration(cell); }) ); From 52ac99fe8cf5b8b6b112238453ce80d1704a80d7 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:21:29 +0200 Subject: [PATCH 09/20] add priority option to all sql cell status bar items --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 9c80417368..b390acb917 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -118,6 +118,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid items.push({ text: `$(database) ${l10n.t('No integration connected')}`, alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 100, tooltip: l10n.t('No SQL integration connected\nClick to select an integration'), command: { title: l10n.t('Switch Integration'), @@ -142,6 +143,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return { text: `$(database) ${l10n.t('DataFrame SQL (DuckDB)')}`, alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 100, tooltip: l10n.t('Internal DuckDB integration for querying DataFrames\nClick to switch'), command: { title: l10n.t('Switch Integration'), @@ -164,6 +166,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return { text: `$(database) ${displayName}`, alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 100, tooltip: l10n.t('SQL Integration: {0}\nClick to switch or configure', displayName), command: { title: l10n.t('Switch Integration'), @@ -179,6 +182,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return { text: l10n.t('Variable: {0}', variableName), alignment: 1, // NotebookCellStatusBarAlignment.Left + priority: 90, tooltip: l10n.t('Variable name for SQL query result\nClick to change'), command: { title: l10n.t('Change Variable Name'), From ffe768101a26b836f0384a2a4da419f07e8a802a Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:42:59 +0200 Subject: [PATCH 10/20] trim var name input --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index b390acb917..1583a4ffeb 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -207,20 +207,22 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid private async updateVariableName(cell: NotebookCell): Promise { const currentVariableName = this.getVariableName(cell); - const newVariableName = await window.showInputBox({ + const newVariableNameInput = await window.showInputBox({ prompt: l10n.t('Enter variable name for SQL query result'), value: currentVariableName, validateInput: (value) => { - if (!value) { + const trimmed = value.trim(); + if (!trimmed) { return l10n.t('Variable name cannot be empty'); } - if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) { + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) { return l10n.t('Variable name must be a valid Python identifier'); } return undefined; } }); + const newVariableName = newVariableNameInput?.trim(); if (newVariableName === undefined || newVariableName === currentVariableName) { return; } From 3343209edcbd631a966fa42763b64f22364bb2ad Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:43:14 +0200 Subject: [PATCH 11/20] hide separator when at the end --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 1583a4ffeb..026665fad5 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -291,14 +291,14 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid id: DATAFRAME_SQL_INTEGRATION_ID } as QuickPickItem & { id: string }); - // Add separator - items.push({ - label: '', - kind: QuickPickItemKind.Separator - }); - - // Add "Configure current integration" option + // Add "Configure current integration" option (with separator) if (currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID) { + // Add separator + items.push({ + label: '', + kind: QuickPickItemKind.Separator + }); + items.push({ label: l10n.t('Configure current integration'), id: '__configure__' From d000e55e00f3f61786c78c8c810a87840013c6e8 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:43:27 +0200 Subject: [PATCH 12/20] add test assertion for status bar item alignment --- src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 6a0a6a6396..9996707ba1 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -184,6 +184,7 @@ suite('SqlCellStatusBarProvider', () => { // Check variable status bar item is still shown const variableItem = items[0]; assert.strictEqual(variableItem.text, 'Variable: df'); + assert.strictEqual(variableItem.alignment, 1); }); test('shows custom variable name when set in metadata', async () => { From 45664198a0a772ac5d8c6eb245c9c1efe4aed1d1 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:43:38 +0200 Subject: [PATCH 13/20] remove fragile test of disposable registration --- .../deepnote/sqlCellStatusBarProvider.unit.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 9996707ba1..5d5f358b0c 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -261,18 +261,6 @@ suite('SqlCellStatusBarProvider', () => { verify(mockedVSCodeNamespaces.commands.registerCommand('deepnote.switchSqlIntegration', anything())).once(); }); - test('adds all registrations to disposables', () => { - activateProvider.activate(); - - // Should have 5 disposables: - // 1. notebook cell status bar provider - // 2. integration storage change listener - // 3. updateSqlVariableName command - // 4. switchSqlIntegration command - // 5. event emitter - assert.strictEqual(activateDisposables.length, 5); - }); - test('listens to integration storage changes', () => { const onDidChangeIntegrations = new EventEmitter(); when(activateIntegrationStorage.onDidChangeIntegrations).thenReturn(onDidChangeIntegrations.event); From 6f8e1836af456f1aafbb95a1c68d371047718ac6 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 16:58:45 +0200 Subject: [PATCH 14/20] avoid updating sql block to the current integration --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 026665fad5..f062df1190 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -322,6 +322,11 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return; } + // No change + if (selectedId === currentIntegrationId) { + return; + } + // Update cell metadata with new integration ID const edit = new WorkspaceEdit(); const updatedMetadata = { From 844ee6d21fc81ab313e19985a7a65be3b4fa6134 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:03:29 +0200 Subject: [PATCH 15/20] test: add assertion of sql status bar item commands and alignments --- .../sqlCellStatusBarProvider.unit.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts index 5d5f358b0c..209ecb3c36 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.unit.test.ts @@ -58,6 +58,8 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(integrationItem.alignment, 1); assert.isDefined(integrationItem.command); assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); + assert.deepStrictEqual(integrationItem.command.arguments, [cell]); + assert.strictEqual(integrationItem.priority, 100); // Check variable status bar item const variableItem = items[1]; @@ -65,6 +67,8 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.alignment, 1); assert.isDefined(variableItem.command); assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); + assert.deepStrictEqual(variableItem.command.arguments, [cell]); + assert.strictEqual(variableItem.priority, 90); }); test('returns status bar items for SQL cells with dataframe integration ID', async () => { @@ -89,6 +93,8 @@ suite('SqlCellStatusBarProvider', () => { ); assert.isDefined(integrationItem.command); assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); + assert.deepStrictEqual(integrationItem.command.arguments, [cell]); + assert.strictEqual(integrationItem.priority, 100); // Check variable status bar item const variableItem = items[1]; @@ -96,6 +102,8 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.alignment, 1); assert.isDefined(variableItem.command); assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); + assert.deepStrictEqual(variableItem.command.arguments, [cell]); + assert.strictEqual(variableItem.priority, 90); }); test('returns status bar items for SQL cell with integration ID', async () => { @@ -135,6 +143,7 @@ suite('SqlCellStatusBarProvider', () => { assert.isDefined(integrationItem.command); assert.strictEqual(integrationItem.command.command, 'deepnote.switchSqlIntegration'); assert.deepStrictEqual(integrationItem.command.arguments, [cell]); + assert.strictEqual(integrationItem.priority, 100); // Check variable status bar item const variableItem = items[1]; @@ -142,6 +151,8 @@ suite('SqlCellStatusBarProvider', () => { assert.strictEqual(variableItem.alignment, 1); assert.isDefined(variableItem.command); assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); + assert.deepStrictEqual(variableItem.command.arguments, [cell]); + assert.strictEqual(variableItem.priority, 90); }); test('shows "Unknown integration (configure)" when config not found', async () => { @@ -165,7 +176,13 @@ suite('SqlCellStatusBarProvider', () => { const items = result as any[]; assert.strictEqual(items.length, 2); assert.strictEqual(items[0].text, '$(database) Unknown integration (configure)'); + assert.strictEqual(items[0].alignment, 1); + assert.strictEqual(items[0].command.command, 'deepnote.switchSqlIntegration'); + assert.deepStrictEqual(items[0].command.arguments, [cell]); assert.strictEqual(items[1].text, 'Variable: df'); + assert.strictEqual(items[1].alignment, 1); + assert.strictEqual(items[1].command.command, 'deepnote.updateSqlVariableName'); + assert.strictEqual(items[1].priority, 90); }); test('returns only variable item when notebook has no project ID', async () => { @@ -185,6 +202,9 @@ suite('SqlCellStatusBarProvider', () => { const variableItem = items[0]; assert.strictEqual(variableItem.text, 'Variable: df'); assert.strictEqual(variableItem.alignment, 1); + assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); + assert.deepStrictEqual(variableItem.command.arguments, [cell]); + assert.strictEqual(variableItem.priority, 90); }); test('shows custom variable name when set in metadata', async () => { @@ -221,6 +241,8 @@ suite('SqlCellStatusBarProvider', () => { // Check variable status bar item shows custom name const variableItem = items[1]; assert.strictEqual(variableItem.text, 'Variable: my_results'); + assert.strictEqual(variableItem.alignment, 1); + assert.strictEqual(variableItem.command.command, 'deepnote.updateSqlVariableName'); }); suite('activate', () => { From 82964524fe7663a72fb3449a25a01d8528030470 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:16:58 +0200 Subject: [PATCH 16/20] refactor use unified iface for quick pick items --- .../deepnote/sqlCellStatusBarProvider.ts | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index f062df1190..91df6e5c5a 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -23,6 +23,13 @@ import { IIntegrationStorage } from './integrations/types'; import { Commands } from '../../platform/common/constants'; import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes'; +/** + * QuickPick item with an integration ID + */ +interface LocalQuickPickItem extends QuickPickItem { + id: string; +} + /** * Provides status bar items for SQL cells showing the integration name and variable name */ @@ -253,7 +260,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid const allIntegrations = await this.integrationStorage.getAll(); // Build quick pick items - const items: QuickPickItem[] = []; + const items: (QuickPickItem | LocalQuickPickItem)[] = []; // Check if current integration is unknown (not in the list) const isCurrentIntegrationUnknown = @@ -263,46 +270,51 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Add current unknown integration first if it exists if (isCurrentIntegrationUnknown && currentIntegrationId) { - items.push({ + const item: LocalQuickPickItem = { label: l10n.t('Unknown integration (configure)'), description: currentIntegrationId, detail: l10n.t('Currently selected'), id: currentIntegrationId - } as QuickPickItem & { id: string }); + }; + items.push(item); } // Add all configured integrations for (const integration of allIntegrations) { const typeLabel = this.getIntegrationTypeLabel(integration.type); - items.push({ + const item: LocalQuickPickItem = { label: integration.name || integration.id, description: typeLabel, detail: integration.id === currentIntegrationId ? l10n.t('Currently selected') : undefined, // Store the integration ID in a custom property id: integration.id - } as QuickPickItem & { id: string }); + }; + items.push(item); } // Add DuckDB integration - items.push({ + const duckDbItem: LocalQuickPickItem = { label: l10n.t('DataFrame SQL (DuckDB)'), description: l10n.t('DuckDB'), detail: currentIntegrationId === DATAFRAME_SQL_INTEGRATION_ID ? l10n.t('Currently selected') : undefined, id: DATAFRAME_SQL_INTEGRATION_ID - } as QuickPickItem & { id: string }); + }; + items.push(duckDbItem); // Add "Configure current integration" option (with separator) if (currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID) { // Add separator - items.push({ + const separator: QuickPickItem = { label: '', kind: QuickPickItemKind.Separator - }); + }; + items.push(separator); - items.push({ + const configureItem: LocalQuickPickItem = { label: l10n.t('Configure current integration'), id: '__configure__' - } as QuickPickItem & { id: string }); + }; + items.push(configureItem); } const selected = await window.showQuickPick(items, { @@ -314,7 +326,13 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid return; } - const selectedId = (selected as QuickPickItem & { id: string }).id; + // Type guard to check if selected item has an id property + if (!('id' in selected)) { + return; + } + + const selectedItem = selected as LocalQuickPickItem; + const selectedId = selectedItem.id; // Handle "Configure current integration" option if (selectedId === '__configure__' && currentIntegrationId) { From 76a26b46eda8d7c72f7e9423cc7d209026481a54 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:34:18 +0200 Subject: [PATCH 17/20] fix: react to external notebook changes --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 91df6e5c5a..615f618c3e 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -4,6 +4,7 @@ import { NotebookCell, NotebookCellStatusBarItem, NotebookCellStatusBarItemProvider, + NotebookDocumentChangeEvent, NotebookEdit, ProviderResult, QuickPickItem, @@ -55,6 +56,15 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid }) ); + // Refresh when any Deepnote notebook changes (e.g., metadata updated externally) + this.disposables.push( + workspace.onDidChangeNotebookDocument((e: NotebookDocumentChangeEvent) => { + if (e.notebook.notebookType === 'deepnote') { + this._onDidChangeCellStatusBarItems.fire(); + } + }) + ); + // Register command to update SQL variable name this.disposables.push( commands.registerCommand('deepnote.updateSqlVariableName', async (cell?: NotebookCell) => { From d6a3f56e659aed135b88826672f13be0b0798a08 Mon Sep 17 00:00:00 2001 From: jankuca Date: Tue, 21 Oct 2025 17:34:34 +0200 Subject: [PATCH 18/20] fix unknown integration type handling --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 615f618c3e..a7c3e780d5 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -381,7 +381,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid case IntegrationType.BigQuery: return l10n.t('BigQuery'); default: - return type; + return String(type); } } } From 47be3cc4fc5dd8af1bd1c83d481d6e24851c7fb4 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 16:11:59 +0200 Subject: [PATCH 19/20] fix command palette support --- .../deepnote/sqlCellStatusBarProvider.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index a7c3e780d5..4cd1f88ac1 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -68,10 +68,19 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Register command to update SQL variable name this.disposables.push( commands.registerCommand('deepnote.updateSqlVariableName', async (cell?: NotebookCell) => { + if (!cell) { + // Fall back to the active notebook cell + const activeEditor = window.activeNotebookEditor; + if (activeEditor && activeEditor.selection) { + cell = activeEditor.notebook.cellAt(activeEditor.selection.start); + } + } + if (!cell) { void window.showErrorMessage(l10n.t('No active notebook cell')); return; } + await this.updateVariableName(cell); }) ); @@ -79,10 +88,19 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid // Register command to switch SQL integration this.disposables.push( commands.registerCommand('deepnote.switchSqlIntegration', async (cell?: NotebookCell) => { + if (!cell) { + // Fall back to the active notebook cell + const activeEditor = window.activeNotebookEditor; + if (activeEditor && activeEditor.selection) { + cell = activeEditor.notebook.cellAt(activeEditor.selection.start); + } + } + if (!cell) { void window.showErrorMessage(l10n.t('No active notebook cell')); return; } + await this.switchIntegration(cell); }) ); From 984f071c3209a0417fe9e29a998b5d7214302ab1 Mon Sep 17 00:00:00 2001 From: jankuca Date: Wed, 22 Oct 2025 16:12:10 +0200 Subject: [PATCH 20/20] avoid auto closing on blur --- src/notebooks/deepnote/sqlCellStatusBarProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts index 4cd1f88ac1..4637b18cbb 100644 --- a/src/notebooks/deepnote/sqlCellStatusBarProvider.ts +++ b/src/notebooks/deepnote/sqlCellStatusBarProvider.ts @@ -245,6 +245,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid const newVariableNameInput = await window.showInputBox({ prompt: l10n.t('Enter variable name for SQL query result'), value: currentVariableName, + ignoreFocusOut: true, validateInput: (value) => { const trimmed = value.trim(); if (!trimmed) {