From 3198df9ba7f07a0bf77ac464856976106e56dd0d Mon Sep 17 00:00:00 2001 From: Bob Cozzi Date: Wed, 29 Apr 2026 12:31:05 -0500 Subject: [PATCH] feat: add Copy as SQL List context menu commands Adds two new right-click editor commands for SQL files when text is selected: - 'Copy as SQL List' - instant clipboard copy using user settings - 'Copy as SQL List (Prompt Format)' - QuickPick with format options Values are single-quoted and internal single quotes are escaped (SQL-safe). When the selection contains numeric values, additional unquoted variants are offered in the prompt. New settings: vscode-db2i.delimitedList.quoteNumbers (default: true) vscode-db2i.delimitedList.wrapInParentheses (default: false) Addresses community request: https://chat.ibmioss.org/#narrow/channel/8-vscode/topic/Seb's.20Extension.20Example.20Part.20deux/with/4188 --- package.json | 38 ++++++++++++ src/contributes.json | 38 ++++++++++++ src/extension.ts | 2 + src/language/clipboard.ts | 118 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+) create mode 100644 src/language/clipboard.ts diff --git a/package.json b/package.json index fdacd5db..88d22b72 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,16 @@ "Java 17", "Java 21" ] + }, + "vscode-db2i.delimitedList.quoteNumbers": { + "type": "boolean", + "description": "Quote numeric values when copying a selection as a SQL list", + "default": true + }, + "vscode-db2i.delimitedList.wrapInParentheses": { + "type": "boolean", + "description": "Wrap the SQL list in parentheses when using 'Copy as SQL List' (instant copy)", + "default": false } } }, @@ -542,6 +552,16 @@ "title": "Generate Insert from File", "category": "Db2 for i" }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "title": "Copy as SQL List", + "category": "Db2 for i" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "title": "Copy as SQL List (Prompt Format)", + "category": "Db2 for i" + }, { "command": "vscode-db2i.refreshSchemaBrowser", "title": "Refresh Schema Browser", @@ -1032,6 +1052,14 @@ "command": "vscode-db2i.importDataContextMenu", "when": "false" }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "when": "editorLangId == sql && editorHasSelection" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "when": "editorLangId == sql && editorHasSelection" + }, { "command": "vscode-db2i.filterDatabaseObjectTypes", "when": "never" @@ -1204,6 +1232,16 @@ { "command": "vscode-db2i.importDataContextMenu", "group": "sql@3" + }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "group": "sql@4", + "when": "editorLangId == sql && editorHasSelection" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "group": "sql@5", + "when": "editorLangId == sql && editorHasSelection" } ], "view/title": [ diff --git a/src/contributes.json b/src/contributes.json index 36d1702a..1ea1af7f 100644 --- a/src/contributes.json +++ b/src/contributes.json @@ -38,6 +38,16 @@ "Java 17", "Java 21" ] + }, + "vscode-db2i.delimitedList.quoteNumbers": { + "type": "boolean", + "description": "Quote numeric values when copying a selection as a SQL list", + "default": true + }, + "vscode-db2i.delimitedList.wrapInParentheses": { + "type": "boolean", + "description": "Wrap the SQL list in parentheses when using 'Copy as SQL List' (instant copy)", + "default": false } } }, @@ -137,6 +147,16 @@ "command": "vscode-db2i.importDataContextMenu", "title": "Generate Insert from File", "category": "Db2 for i" + }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "title": "Copy as SQL List", + "category": "Db2 for i" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "title": "Copy as SQL List (Prompt Format)", + "category": "Db2 for i" } ], "menus": { @@ -148,6 +168,14 @@ { "command": "vscode-db2i.importDataContextMenu", "when": "false" + }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "when": "editorLangId == sql && editorHasSelection" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "when": "editorLangId == sql && editorHasSelection" } ], "editor/context": [ @@ -169,6 +197,16 @@ { "command": "vscode-db2i.importDataContextMenu", "group": "sql@3" + }, + { + "command": "vscode-db2i.copyAsDelimitedList", + "group": "sql@4", + "when": "editorLangId == sql && editorHasSelection" + }, + { + "command": "vscode-db2i.copyAsDelimitedListPrompt", + "group": "sql@5", + "when": "editorLangId == sql && editorHasSelection" } ] } diff --git a/src/extension.ts b/src/extension.ts index 55a393d1..057f5277 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; import SchemaBrowser from "./views/schemaBrowser"; import * as JSONServices from "./language/json"; +import * as ClipboardServices from "./language/clipboard"; import * as resultsProvider from "./views/results"; import { JDBCOptions } from "@ibm/mapepire-js/dist/src/types"; @@ -73,6 +74,7 @@ export function activate(context: vscode.ExtensionContext): Db2i { ); JSONServices.initialise(context); + ClipboardServices.initialise(context); resultsProvider.initialise(context); initConfig(context); diff --git a/src/language/clipboard.ts b/src/language/clipboard.ts new file mode 100644 index 00000000..2b025932 --- /dev/null +++ b/src/language/clipboard.ts @@ -0,0 +1,118 @@ +import * as vscode from "vscode"; +import Configuration from "../configuration"; + +const NUMERIC_RE = /^\d+(\.\d+)?$/; +const LARGE_SELECTION_THRESHOLD = 256; // Just for safety + +function getLines(selectedText: string): string[] { + return selectedText + .split(/\r?\n/) + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0); +} + +function quoteLine(line: string): string { + return `'${line.replace(/'/g, `''`)}'`; +} + +function buildList(lines: string[], quoteNumbers: boolean, wrap: boolean): string { + const values = lines.map((line: string) => + !quoteNumbers && NUMERIC_RE.test(line) ? line : quoteLine(line) + ); + const csv = values.join(`, `); + return wrap ? `(${csv})` : csv; +} + +async function writeToClipboard(text: string): Promise { + try { + await vscode.env.clipboard.writeText(text); + return true; + } catch (e) { + vscode.window.showErrorMessage(`Failed to write to clipboard: ${e instanceof Error ? e.message : String(e)}`); + return false; + } +} + +async function confirmLargeSelection(count: number): Promise { + if (count <= LARGE_SELECTION_THRESHOLD) return true; + const answer = await vscode.window.showWarningMessage( + `Your selection contains ${count} values. Continue copying to clipboard?`, + { modal: true }, + `Continue` + ); + return answer === `Continue`; +} + +export function initialise(context: vscode.ExtensionContext) { + context.subscriptions.push( + + // ── Instant copy using current settings ────────────────────────────────── + vscode.commands.registerCommand(`vscode-db2i.copyAsDelimitedList`, async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) return; + + const selectedText = editor.document.getText(editor.selection); + const lines = getLines(selectedText); + + if (lines.length === 0) { + vscode.window.showErrorMessage(`No text selected.`); + return; + } + + if (!await confirmLargeSelection(lines.length)) return; + + const quoteNumbers = Configuration.get(`delimitedList.quoteNumbers`) ?? true; + const wrapInParens = Configuration.get(`delimitedList.wrapInParentheses`) ?? false; + + const result = buildList(lines, quoteNumbers, wrapInParens); + if (await writeToClipboard(result)) { + vscode.window.showInformationMessage(`SQL list copied to clipboard.`); + } + }), + + // ── Prompted copy with format options ──────────────────────────────────── + vscode.commands.registerCommand(`vscode-db2i.copyAsDelimitedListPrompt`, async () => { + const editor = vscode.window.activeTextEditor; + if (!editor) return; + + const selectedText = editor.document.getText(editor.selection); + const lines = getLines(selectedText); + + if (lines.length === 0) { + vscode.window.showErrorMessage(`No text selected.`); + return; + } + + if (!await confirmLargeSelection(lines.length)) return; + + const hasNumbers = lines.some((line: string) => NUMERIC_RE.test(line)); + + const listQuoted = buildList(lines, true, false); + const listQuotedParens = buildList(lines, true, true); + + type QuickPickItem = { label: string; description: string; value: string }; + const options: QuickPickItem[] = [ + { label: `Copy as list`, description: listQuoted, value: listQuoted }, + ]; + + if (hasNumbers) { + const listUnquoted = buildList(lines, false, false); + const listUnquotedParens = buildList(lines, false, true); + options.push( + { label: `Copy as list (numbers unquoted)`, description: listUnquoted, value: listUnquoted }, + { label: `Copy wrapped in parentheses`, description: listQuotedParens, value: listQuotedParens }, + { label: `Copy wrapped in parentheses (numbers unquoted)`, description: listUnquotedParens, value: listUnquotedParens } + ); + } else { + options.push( + { label: `Copy wrapped in parentheses`, description: listQuotedParens, value: listQuotedParens } + ); + } + + const choice = await vscode.window.showQuickPick(options, { title: `Copy as SQL List` }); + if (choice && await writeToClipboard(choice.value)) { + vscode.window.showInformationMessage(`SQL list copied to clipboard.`); + } + }) + ); +}