@@ -4,21 +4,35 @@ import {
44 NotebookCell ,
55 NotebookCellStatusBarItem ,
66 NotebookCellStatusBarItemProvider ,
7- NotebookDocument ,
7+ NotebookDocumentChangeEvent ,
8+ NotebookEdit ,
89 ProviderResult ,
10+ QuickPickItem ,
11+ QuickPickItemKind ,
12+ WorkspaceEdit ,
13+ commands ,
914 l10n ,
10- notebooks
15+ notebooks ,
16+ window ,
17+ workspace
1118} from 'vscode' ;
1219import { inject , injectable } from 'inversify' ;
1320
1421import { IExtensionSyncActivationService } from '../../platform/activation/types' ;
1522import { IDisposableRegistry } from '../../platform/common/types' ;
16- import { Commands } from '../../platform/common/constants' ;
1723import { IIntegrationStorage } from './integrations/types' ;
18- import { DATAFRAME_SQL_INTEGRATION_ID } from '../../platform/notebooks/deepnote/integrationTypes' ;
24+ import { Commands } from '../../platform/common/constants' ;
25+ import { DATAFRAME_SQL_INTEGRATION_ID , IntegrationType } from '../../platform/notebooks/deepnote/integrationTypes' ;
1926
2027/**
21- * Provides status bar items for SQL cells showing the integration name
28+ * QuickPick item with an integration ID
29+ */
30+ interface LocalQuickPickItem extends QuickPickItem {
31+ id : string ;
32+ }
33+
34+ /**
35+ * Provides status bar items for SQL cells showing the integration name and variable name
2236 */
2337@injectable ( )
2438export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvider , IExtensionSyncActivationService {
@@ -42,6 +56,55 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
4256 } )
4357 ) ;
4458
59+ // Refresh when any Deepnote notebook changes (e.g., metadata updated externally)
60+ this . disposables . push (
61+ workspace . onDidChangeNotebookDocument ( ( e : NotebookDocumentChangeEvent ) => {
62+ if ( e . notebook . notebookType === 'deepnote' ) {
63+ this . _onDidChangeCellStatusBarItems . fire ( ) ;
64+ }
65+ } )
66+ ) ;
67+
68+ // Register command to update SQL variable name
69+ this . disposables . push (
70+ commands . registerCommand ( 'deepnote.updateSqlVariableName' , async ( cell ?: NotebookCell ) => {
71+ if ( ! cell ) {
72+ // Fall back to the active notebook cell
73+ const activeEditor = window . activeNotebookEditor ;
74+ if ( activeEditor && activeEditor . selection ) {
75+ cell = activeEditor . notebook . cellAt ( activeEditor . selection . start ) ;
76+ }
77+ }
78+
79+ if ( ! cell ) {
80+ void window . showErrorMessage ( l10n . t ( 'No active notebook cell' ) ) ;
81+ return ;
82+ }
83+
84+ await this . updateVariableName ( cell ) ;
85+ } )
86+ ) ;
87+
88+ // Register command to switch SQL integration
89+ this . disposables . push (
90+ commands . registerCommand ( 'deepnote.switchSqlIntegration' , async ( cell ?: NotebookCell ) => {
91+ if ( ! cell ) {
92+ // Fall back to the active notebook cell
93+ const activeEditor = window . activeNotebookEditor ;
94+ if ( activeEditor && activeEditor . selection ) {
95+ cell = activeEditor . notebook . cellAt ( activeEditor . selection . start ) ;
96+ }
97+ }
98+
99+ if ( ! cell ) {
100+ void window . showErrorMessage ( l10n . t ( 'No active notebook cell' ) ) ;
101+ return ;
102+ }
103+
104+ await this . switchIntegration ( cell ) ;
105+ } )
106+ ) ;
107+
45108 // Dispose our emitter with the extension
46109 this . disposables . push ( this . _onDidChangeCellStatusBarItems ) ;
47110 }
@@ -59,18 +122,7 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
59122 return undefined ;
60123 }
61124
62- // Get the integration ID from cell metadata
63- const integrationId = this . getIntegrationId ( cell ) ;
64- if ( ! integrationId ) {
65- return undefined ;
66- }
67-
68- // Don't show status bar for the internal DuckDB integration
69- if ( integrationId === DATAFRAME_SQL_INTEGRATION_ID ) {
70- return undefined ;
71- }
72-
73- return this . createStatusBarItem ( cell . notebook , integrationId ) ;
125+ return this . createStatusBarItems ( cell ) ;
74126 }
75127
76128 private getIntegrationId ( cell : NotebookCell ) : string | undefined {
@@ -86,11 +138,57 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
86138 return undefined ;
87139 }
88140
89- private async createStatusBarItem (
90- notebook : NotebookDocument ,
141+ private async createStatusBarItems ( cell : NotebookCell ) : Promise < NotebookCellStatusBarItem [ ] > {
142+ const items : NotebookCellStatusBarItem [ ] = [ ] ;
143+
144+ // Add integration status bar item
145+ const integrationId = this . getIntegrationId ( cell ) ;
146+ if ( integrationId ) {
147+ const integrationItem = await this . createIntegrationStatusBarItem ( cell , integrationId ) ;
148+ if ( integrationItem ) {
149+ items . push ( integrationItem ) ;
150+ }
151+ } else {
152+ // Show "No integration connected" when no integration is selected
153+ items . push ( {
154+ text : `$(database) ${ l10n . t ( 'No integration connected' ) } ` ,
155+ alignment : 1 , // NotebookCellStatusBarAlignment.Left
156+ priority : 100 ,
157+ tooltip : l10n . t ( 'No SQL integration connected\nClick to select an integration' ) ,
158+ command : {
159+ title : l10n . t ( 'Switch Integration' ) ,
160+ command : 'deepnote.switchSqlIntegration' ,
161+ arguments : [ cell ]
162+ }
163+ } ) ;
164+ }
165+
166+ // Always add variable status bar item for SQL cells
167+ items . push ( this . createVariableStatusBarItem ( cell ) ) ;
168+
169+ return items ;
170+ }
171+
172+ private async createIntegrationStatusBarItem (
173+ cell : NotebookCell ,
91174 integrationId : string
92175 ) : Promise < NotebookCellStatusBarItem | undefined > {
93- const projectId = notebook . metadata ?. deepnoteProjectId ;
176+ // Handle internal DuckDB integration specially
177+ if ( integrationId === DATAFRAME_SQL_INTEGRATION_ID ) {
178+ return {
179+ text : `$(database) ${ l10n . t ( 'DataFrame SQL (DuckDB)' ) } ` ,
180+ alignment : 1 , // NotebookCellStatusBarAlignment.Left
181+ priority : 100 ,
182+ tooltip : l10n . t ( 'Internal DuckDB integration for querying DataFrames\nClick to switch' ) ,
183+ command : {
184+ title : l10n . t ( 'Switch Integration' ) ,
185+ command : 'deepnote.switchSqlIntegration' ,
186+ arguments : [ cell ]
187+ }
188+ } ;
189+ }
190+
191+ const projectId = cell . notebook . metadata ?. deepnoteProjectId ;
94192 if ( ! projectId ) {
95193 return undefined ;
96194 }
@@ -99,16 +197,210 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
99197 const config = await this . integrationStorage . getProjectIntegrationConfig ( projectId , integrationId ) ;
100198 const displayName = config ?. name || l10n . t ( 'Unknown integration (configure)' ) ;
101199
102- // Create a status bar item that opens the integration management UI
200+ // Create a status bar item that opens the integration picker
103201 return {
104202 text : `$(database) ${ displayName } ` ,
105203 alignment : 1 , // NotebookCellStatusBarAlignment.Left
106- tooltip : l10n . t ( 'SQL Integration: {0}\nClick to configure' , displayName ) ,
204+ priority : 100 ,
205+ tooltip : l10n . t ( 'SQL Integration: {0}\nClick to switch or configure' , displayName ) ,
206+ command : {
207+ title : l10n . t ( 'Switch Integration' ) ,
208+ command : 'deepnote.switchSqlIntegration' ,
209+ arguments : [ cell ]
210+ }
211+ } ;
212+ }
213+
214+ private createVariableStatusBarItem ( cell : NotebookCell ) : NotebookCellStatusBarItem {
215+ const variableName = this . getVariableName ( cell ) ;
216+
217+ return {
218+ text : l10n . t ( 'Variable: {0}' , variableName ) ,
219+ alignment : 1 , // NotebookCellStatusBarAlignment.Left
220+ priority : 90 ,
221+ tooltip : l10n . t ( 'Variable name for SQL query result\nClick to change' ) ,
107222 command : {
108- title : l10n . t ( 'Configure Integration' ) ,
109- command : Commands . ManageIntegrations ,
110- arguments : [ integrationId ]
223+ title : l10n . t ( 'Change Variable Name' ) ,
224+ command : 'deepnote.updateSqlVariableName' ,
225+ arguments : [ cell ]
226+ }
227+ } ;
228+ }
229+
230+ private getVariableName ( cell : NotebookCell ) : string {
231+ const metadata = cell . metadata ;
232+ if ( metadata && typeof metadata === 'object' ) {
233+ const variableName = ( metadata as Record < string , unknown > ) . deepnote_variable_name ;
234+ if ( typeof variableName === 'string' && variableName ) {
235+ return variableName ;
111236 }
237+ }
238+
239+ return 'df' ;
240+ }
241+
242+ private async updateVariableName ( cell : NotebookCell ) : Promise < void > {
243+ const currentVariableName = this . getVariableName ( cell ) ;
244+
245+ const newVariableNameInput = await window . showInputBox ( {
246+ prompt : l10n . t ( 'Enter variable name for SQL query result' ) ,
247+ value : currentVariableName ,
248+ ignoreFocusOut : true ,
249+ validateInput : ( value ) => {
250+ const trimmed = value . trim ( ) ;
251+ if ( ! trimmed ) {
252+ return l10n . t ( 'Variable name cannot be empty' ) ;
253+ }
254+ if ( ! / ^ [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * $ / . test ( trimmed ) ) {
255+ return l10n . t ( 'Variable name must be a valid Python identifier' ) ;
256+ }
257+ return undefined ;
258+ }
259+ } ) ;
260+
261+ const newVariableName = newVariableNameInput ?. trim ( ) ;
262+ if ( newVariableName === undefined || newVariableName === currentVariableName ) {
263+ return ;
264+ }
265+
266+ // Update cell metadata
267+ const edit = new WorkspaceEdit ( ) ;
268+ const updatedMetadata = {
269+ ...cell . metadata ,
270+ deepnote_variable_name : newVariableName
112271 } ;
272+
273+ edit . set ( cell . notebook . uri , [ NotebookEdit . updateCellMetadata ( cell . index , updatedMetadata ) ] ) ;
274+
275+ const success = await workspace . applyEdit ( edit ) ;
276+ if ( ! success ) {
277+ void window . showErrorMessage ( l10n . t ( 'Failed to update variable name' ) ) ;
278+ return ;
279+ }
280+
281+ // Trigger status bar update
282+ this . _onDidChangeCellStatusBarItems . fire ( ) ;
283+ }
284+
285+ private async switchIntegration ( cell : NotebookCell ) : Promise < void > {
286+ const currentIntegrationId = this . getIntegrationId ( cell ) ;
287+
288+ // Get all available integrations
289+ const allIntegrations = await this . integrationStorage . getAll ( ) ;
290+
291+ // Build quick pick items
292+ const items : ( QuickPickItem | LocalQuickPickItem ) [ ] = [ ] ;
293+
294+ // Check if current integration is unknown (not in the list)
295+ const isCurrentIntegrationUnknown =
296+ currentIntegrationId &&
297+ currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID &&
298+ ! allIntegrations . some ( ( i ) => i . id === currentIntegrationId ) ;
299+
300+ // Add current unknown integration first if it exists
301+ if ( isCurrentIntegrationUnknown && currentIntegrationId ) {
302+ const item : LocalQuickPickItem = {
303+ label : l10n . t ( 'Unknown integration (configure)' ) ,
304+ description : currentIntegrationId ,
305+ detail : l10n . t ( 'Currently selected' ) ,
306+ id : currentIntegrationId
307+ } ;
308+ items . push ( item ) ;
309+ }
310+
311+ // Add all configured integrations
312+ for ( const integration of allIntegrations ) {
313+ const typeLabel = this . getIntegrationTypeLabel ( integration . type ) ;
314+ const item : LocalQuickPickItem = {
315+ label : integration . name || integration . id ,
316+ description : typeLabel ,
317+ detail : integration . id === currentIntegrationId ? l10n . t ( 'Currently selected' ) : undefined ,
318+ // Store the integration ID in a custom property
319+ id : integration . id
320+ } ;
321+ items . push ( item ) ;
322+ }
323+
324+ // Add DuckDB integration
325+ const duckDbItem : LocalQuickPickItem = {
326+ label : l10n . t ( 'DataFrame SQL (DuckDB)' ) ,
327+ description : l10n . t ( 'DuckDB' ) ,
328+ detail : currentIntegrationId === DATAFRAME_SQL_INTEGRATION_ID ? l10n . t ( 'Currently selected' ) : undefined ,
329+ id : DATAFRAME_SQL_INTEGRATION_ID
330+ } ;
331+ items . push ( duckDbItem ) ;
332+
333+ // Add "Configure current integration" option (with separator)
334+ if ( currentIntegrationId && currentIntegrationId !== DATAFRAME_SQL_INTEGRATION_ID ) {
335+ // Add separator
336+ const separator : QuickPickItem = {
337+ label : '' ,
338+ kind : QuickPickItemKind . Separator
339+ } ;
340+ items . push ( separator ) ;
341+
342+ const configureItem : LocalQuickPickItem = {
343+ label : l10n . t ( 'Configure current integration' ) ,
344+ id : '__configure__'
345+ } ;
346+ items . push ( configureItem ) ;
347+ }
348+
349+ const selected = await window . showQuickPick ( items , {
350+ placeHolder : l10n . t ( 'Select SQL integration' ) ,
351+ matchOnDescription : true
352+ } ) ;
353+
354+ if ( ! selected ) {
355+ return ;
356+ }
357+
358+ // Type guard to check if selected item has an id property
359+ if ( ! ( 'id' in selected ) ) {
360+ return ;
361+ }
362+
363+ const selectedItem = selected as LocalQuickPickItem ;
364+ const selectedId = selectedItem . id ;
365+
366+ // Handle "Configure current integration" option
367+ if ( selectedId === '__configure__' && currentIntegrationId ) {
368+ await commands . executeCommand ( Commands . ManageIntegrations , currentIntegrationId ) ;
369+ return ;
370+ }
371+
372+ // No change
373+ if ( selectedId === currentIntegrationId ) {
374+ return ;
375+ }
376+
377+ // Update cell metadata with new integration ID
378+ const edit = new WorkspaceEdit ( ) ;
379+ const updatedMetadata = {
380+ ...cell . metadata ,
381+ sql_integration_id : selectedId
382+ } ;
383+
384+ edit . set ( cell . notebook . uri , [ NotebookEdit . updateCellMetadata ( cell . index , updatedMetadata ) ] ) ;
385+
386+ const success = await workspace . applyEdit ( edit ) ;
387+ if ( ! success ) {
388+ void window . showErrorMessage ( l10n . t ( 'Failed to select integration' ) ) ;
389+ return ;
390+ }
391+
392+ // Trigger status bar update
393+ this . _onDidChangeCellStatusBarItems . fire ( ) ;
394+ }
395+
396+ private getIntegrationTypeLabel ( type : IntegrationType ) : string {
397+ switch ( type ) {
398+ case IntegrationType . Postgres :
399+ return l10n . t ( 'PostgreSQL' ) ;
400+ case IntegrationType . BigQuery :
401+ return l10n . t ( 'BigQuery' ) ;
402+ default :
403+ return String ( type ) ;
404+ }
113405 }
114406}
0 commit comments