Skip to content

data explorer: create "Copy as Code" modal #8537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 66 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
02a2c28
make action and button
isabelizimm Jul 2, 2025
dc9ef57
Merge branch 'main' into export-to-code
isabelizimm Jul 8, 2025
6f502e0
plumb through comms
isabelizimm Jul 9, 2025
dcf380b
export -> copy
isabelizimm Jul 10, 2025
32eb2c0
add modal on filter bar
isabelizimm Jul 11, 2025
f9a2c01
ui updates
isabelizimm Jul 14, 2025
2b9f962
make calls from modal
isabelizimm Jul 15, 2025
5083889
clean up comms
isabelizimm Jul 15, 2025
5be588b
clean up modal
isabelizimm Jul 15, 2025
d59fd66
linting for python
isabelizimm Jul 15, 2025
cc2d069
nit to modal
isabelizimm Jul 15, 2025
bae6ced
Merge branch 'export-as-code-comms' into copy-as-code-comms
isabelizimm Jul 15, 2025
1c0c11a
add modal into new branch
isabelizimm Jul 15, 2025
35a2114
update names and comments
isabelizimm Jul 15, 2025
4d624bc
updates from copilot
isabelizimm Jul 15, 2025
ac12c66
nit
isabelizimm Jul 15, 2025
0d6e7d9
cleanup
isabelizimm Jul 15, 2025
2e71d12
rename title creation
isabelizimm Jul 15, 2025
99e782f
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 15, 2025
c33df7c
updates from review
isabelizimm Jul 16, 2025
45cd402
guess syntax immediately
isabelizimm Jul 16, 2025
d75c201
updates from review
isabelizimm Jul 16, 2025
87ae32f
guess syntax immediately
isabelizimm Jul 16, 2025
5d9eaa5
rearrange with new updates
isabelizimm Jul 16, 2025
1adfc02
reorder guessing
isabelizimm Jul 16, 2025
7a0d40b
no undefined for guessing
isabelizimm Jul 16, 2025
47dc3e1
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 16, 2025
776a44e
cleanup modal
isabelizimm Jul 16, 2025
273b988
remove unused action id
isabelizimm Jul 16, 2025
9cbe02a
python lint
isabelizimm Jul 17, 2025
51893f0
Update positron/comms/data_explorer-backend-openrpc.json
isabelizimm Jul 17, 2025
9517202
rename, early exits
isabelizimm Jul 17, 2025
36bf5f5
use supported feature flag for syntaxes
isabelizimm Jul 17, 2025
da70e52
refactor
isabelizimm Jul 17, 2025
b6bbe5c
clean up comm names
isabelizimm Jul 17, 2025
6e5bcfc
Apply suggestions from code review
isabelizimm Jul 18, 2025
1525ad6
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 17, 2025
e85e6fe
fixes with comm updates
isabelizimm Jul 17, 2025
fbaf3cf
updates from review
isabelizimm Jul 18, 2025
50974b7
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 18, 2025
ccd15c4
updates from review
isabelizimm Jul 18, 2025
281ddac
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 18, 2025
b3aa955
updates to modal
isabelizimm Jul 18, 2025
0cc6f14
tweaks to runtime
isabelizimm Jul 18, 2025
744cc66
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 18, 2025
0c0ffac
Merge branch 'main' into copy-as-code-comms
isabelizimm Jul 18, 2025
a85ef9f
lint post merge from main
isabelizimm Jul 18, 2025
8806973
Merge branch 'copy-as-code-comms' into copy-as-code-modal
isabelizimm Jul 18, 2025
f8286a8
add feature flag for convert to code action
isabelizimm Jul 18, 2025
c108b91
clean up renderer
isabelizimm Jul 18, 2025
4f6c40a
rename files
isabelizimm Jul 18, 2025
38efe31
Update src/vs/workbench/browser/positronModalDialogs/convertToCodeMod…
isabelizimm Jul 21, 2025
7f394bd
Fix ESLint import warning
dhruvisompura Jul 19, 2025
28739b4
mark duckdb backends as unsupported
isabelizimm Jul 21, 2025
6115af3
clean up actions
isabelizimm Jul 21, 2025
4363bb7
always get code string
isabelizimm Jul 21, 2025
74027f9
only show for supported backends
isabelizimm Jul 21, 2025
e978ca5
Merge branch 'main' into copy-as-code-modal
isabelizimm Jul 21, 2025
781d7ae
Merge branch 'main' into copy-as-code-modal
isabelizimm Jul 22, 2025
3d01ba4
add precondition for syntaxes
isabelizimm Jul 23, 2025
2037e80
nit if no supported features
isabelizimm Jul 23, 2025
d8363e0
remove button if convertocode is missing
isabelizimm Jul 23, 2025
02c4686
remove unecessary services
isabelizimm Jul 23, 2025
ecb7bc1
updates from review
isabelizimm Jul 23, 2025
9b675eb
simplifying dropdown entry and onAccept
isabelizimm Jul 23, 2025
98de592
rearrange context key
isabelizimm Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/positron-duckdb/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,9 @@ END`;
export_data_selection: {
support_status: SupportStatus.Unsupported,
supported_formats: []
},
convert_to_code: {
support_status: SupportStatus.Unsupported,
}
}
};
Expand Down Expand Up @@ -1179,6 +1182,9 @@ END`;
ExportFormat.Tsv,
ExportFormat.Html
]
},
convert_to_code: {
support_status: SupportStatus.Unsupported,
}
}
};
Expand Down
23 changes: 23 additions & 0 deletions extensions/positron-duckdb/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,11 @@ export interface SupportedFeatures {
*/
export_data_selection: ExportDataSelectionFeatures;

/**
* Support for 'convert_to_code' RPC and its features
*/
convert_to_code: ConvertToCodeFeatures;

}

/**
Expand Down Expand Up @@ -1000,6 +1005,24 @@ export interface SetSortColumnsFeatures {

}

/**
* Feature flags for 'convert_to_code' RPC
*/
export interface ConvertToCodeFeatures {
/**
* The support status for this RPC method
* */
support_status: SupportStatus;
/**
* The supported code syntax names
*/
supported_code_syntaxes?: Array<CodeSyntaxName>;
}

export interface CodeSyntaxName {
code_syntax_name: string;
}

/**
* A selection on the data grid, for copying to the clipboard or other
* actions
Expand Down
5 changes: 3 additions & 2 deletions extensions/positron-duckdb/src/test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function createTempTable(
for (let i = 0; i < length; i++) {
tuples.push(`(${columns.map(c => c.values[i]).join(', ')})`);
}

// Use explicit column names in INSERT to ensure proper ordering
const columnNames = columns.map(c => quoteIdentifier(c.name)).join(', ');
await runQuery(`INSERT INTO ${tableName} (${columnNames}) VALUES\n${tuples.join(',\n')};`);
Expand Down Expand Up @@ -243,7 +243,8 @@ suite('Positron DuckDB Extension Test Suite', () => {
ExportFormat.Tsv,
ExportFormat.Html
]
}
},
convert_to_code: { support_status: SupportStatus.Unsupported }
}
} satisfies BackendState);

Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a similar component that the nee folder flow modal uses and its pretty similar to this one. I think there's opportunity here to consolidate both components. We could move this component into the same place where the dropdownlistbox component lives.

This can be a piece of polish/cleanup we do - not necessarily in this PR

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

// React.
import React from 'react';

/**
* DropdownEntryProps interface.
*/
interface DropdownEntryProps {
title: string;
subtitle?: string;
group?: string;
}

/**
* DropdownEntry component.
* @param props The dropdown entry props.
* @returns The rendered component
*/
export const DropdownEntry = (props: DropdownEntryProps) => {
// Render.
return (
<div className='dropdown-entry'>
<div className='dropdown-entry-title'>
{props.title}
</div>
{props.group ? <div className='dropdown-entry-group'>{props.group}</div> : null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified as:

{props.group && <div className='dropdown-entry-group'>{props.group}</div>}

</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

// React.
import React, { useEffect, useState } from 'react';

// Other dependencies.
import { localize } from '../../../nls.js';
import { VerticalStack } from '../positronComponents/positronModalDialog/components/verticalStack.js';
import { OKCancelModalDialog } from '../positronComponents/positronModalDialog/positronOKCancelModalDialog.js';
import { IPositronDataExplorerInstance } from '../../services/positronDataExplorer/browser/interfaces/positronDataExplorerInstance.js';
import { PositronDataExplorerCommandId } from '../../contrib/positronDataExplorerEditor/browser/positronDataExplorerActions.js';
import { DropDownListBox } from '../positronComponents/dropDownListBox/dropDownListBox.js';
import { DropDownListBoxItem } from '../positronComponents/dropDownListBox/dropDownListBoxItem.js';
import { DropdownEntry } from './components/dropdownEntry.js';
import { CodeSyntaxName } from '../../services/languageRuntime/common/positronDataExplorerComm.js';
import { PositronModalReactRenderer } from '../../../base/browser/positronModalReactRenderer.js';
import { usePositronReactServicesContext } from '../../../base/browser/positronReactRendererContext.js';

/**
* Shows the convert to code modal dialog.
* @param dataExplorerClientInstance The data explorer client instance.
* @returns A promise that resolves when the dialog is closed.
*/
export const showConvertToCodeModalDialog = async (
dataExplorerClientInstance: IPositronDataExplorerInstance,
): Promise<void> => {
// Create the renderer.
const renderer = new PositronModalReactRenderer()

// Show the copy as code dialog.
renderer.render(
<ConvertToCodeModalDialog
dataExplorerClientInstance={dataExplorerClientInstance}
renderer={renderer}
/>
);
};

/**
* ConvertToCodeDialogProps interface.
*/
interface ConvertToCodeDialogProps {
dataExplorerClientInstance: IPositronDataExplorerInstance
renderer: PositronModalReactRenderer;
}


/**
* ConvertToCodeModalDialog component.
* @param props The component properties.
* @returns The rendered component.
*/
export const ConvertToCodeModalDialog = (props: ConvertToCodeDialogProps) => {
// Service hooks.
const services = usePositronReactServicesContext();

// State hooks.
const instance = props.dataExplorerClientInstance.dataExplorerClientInstance;
const codeSyntaxOptions = instance.cachedBackendState?.supported_features?.convert_to_code?.code_syntaxes ?? [];

const [selectedSyntax, setSelectedSyntax] = useState<CodeSyntaxName | undefined>(instance.suggestedSyntax);

const [codeString, setCodeString] = useState<string | undefined>(undefined);

useEffect(() => {
const getCodeString = async () => {
try {
// Execute the command to get the code string based on the selected syntax.
const result = await services.commandService.executeCommand(PositronDataExplorerCommandId.ConvertToCodeAction, selectedSyntax);
setCodeString(result);
} catch (error) {
if (selectedSyntax) {
setCodeString(localize(
'positron.dataExplorer.getCodeStringWithSyntax',
"Cannot generate code for type {0}",
selectedSyntax.code_syntax_name
));
} else {
setCodeString(localize(
'positron.dataExplorer.getCodeStringNoSyntax',
"Cannot generate code"
));
}
}
};

getCodeString(); // Call the async function
}, [selectedSyntax, services.commandService]);

// Construct the syntax options dropdown entries
const syntaxDropdownEntries = () => {
return syntaxInfoToDropDownItems(codeSyntaxOptions);
};

const syntaxInfoToDropDownItems = (
syntaxes: CodeSyntaxName[]
): DropDownListBoxItem<string, CodeSyntaxName>[] => {
return syntaxes.map(
(syntax) =>
new DropDownListBoxItem<string, CodeSyntaxName>({
identifier: syntax.code_syntax_name,
value: syntax,
})
);
};

const syntaxDropdownTitle = (): string => {
// if selectedSyntax is an object with code_syntax_name, return that name
if (typeof selectedSyntax === 'object' && 'code_syntax_name' in selectedSyntax) {
return (selectedSyntax as CodeSyntaxName).code_syntax_name;
}
return localize('positron.dataExplorer.selectCodeSyntax', 'Select Code Syntax');
}

const onSelectionChanged = async (item: DropDownListBoxItem<string, CodeSyntaxName>) => {
const typedItem = item as DropDownListBoxItem<string, CodeSyntaxName>;
setSelectedSyntax(typedItem.options.value);

// Execute the command to get the code string based on the selected syntax.
try {
const exc = await services.commandService.executeCommand(PositronDataExplorerCommandId.ConvertToCodeAction, typedItem.options.value);
setCodeString(exc);
} catch (error) {
setCodeString(localize(
'positron.dataExplorer.cannotGenerateCodeForType',
"Cannot generate code for type {0}",
typedItem.options.value.code_syntax_name
));
}
};

// Render.
return (
<OKCancelModalDialog
catchErrors
height={300}
renderer={props.renderer}
title={(() => localize(
'positronConvertToCodeModalDialogTitle',
"Convert to Code"
))()}
width={400}
onAccept={() => props.renderer.dispose()}
onCancel={() => props.renderer.dispose()}
>
<VerticalStack>
<DropDownListBox
createItem={(item) => (
<DropdownEntry
title={item.options.identifier}
/>
)}
entries={syntaxDropdownEntries()}
title={syntaxDropdownTitle()}
onSelectionChanged={onSelectionChanged}
/>
<pre>
{codeString}
</pre>
</VerticalStack>

</OKCancelModalDialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const DropdownEntry = (props: DropdownEntryProps) => {
<div className='dropdown-entry-subtitle'>
{props.subtitle}
</div>
{props.group ? <div className='dropdown-entry-group'>{props.group}</div> : null}
{props.group && <div className='dropdown-entry-group'>{props.group}</div>}
</div>
);
};
Loading
Loading