diff --git a/README.md b/README.md index e0e9f16..6a65406 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ See more usage examples in [the docs directory](https://github.com/hilleer/vscod - Convert a selection of YAML files to JSON by right clicking one of the selected files and selecting `Convert selected files to JSON`. - Convert YAML files in a directory to JSON by right clicking the directory and selecting `Convert YAML files to JSON`. - Convert JSON files in a directory to YAML by right clicking the directory and selecting `Convert JSON files to YAML`. + +- **Interactive converter:** Open a persistent split-pane panel to convert arbitrary YAML/JSON content without needing a file on disk. Click the `⇄` icon in the editor toolbar or run `YAML+JSON: Open Interactive Converter` from the command palette. Pre-populates from the active editor, supports real-time conversion, Swap, Copy, and Save As. - **Comment preservation (YAML to or from JSONC):** - When converting between YAML and JSONC, comments are preserved by default (`preserveComments: true`). - YAML `#` comments map to JSONC `//` comments and vice versa. @@ -43,6 +45,12 @@ See more usage examples in [the docs directory](https://github.com/hilleer/vscod - **Reverting converted files:** When a file has been reverted, a _"revert"_ prompt will be shown to revert it. Using this will return the entirety of the original file, including YAML comments. - **Overwriting existent files:** When trying to convert a file into a destination that already exist, you can use the `overwriteExistentFiles` configuration to overwrite such. **Notice** if you use the revert feature after overwriting a file, the extension cannot (currently) revert the overwritten file. Also, due to limitation in vscode of active user prompts, if you set it to `"ask"` you will only be prompted to overwrite N number of files, while others will be skipped. +## Usage examples + +**Converting files via the interactive tool:** + + + ## Config All configurations should be defined in the `yaml-plus-json` object of your vscode settings (e.g. in a workspace file `.vscode/settings.json`). Example: diff --git a/docs/demo_interactive.gif b/docs/demo_interactive.gif new file mode 100644 index 0000000..0de0dd0 Binary files /dev/null and b/docs/demo_interactive.gif differ diff --git a/package.json b/package.json index d1f9dc5..bf70fa5 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,11 @@ { "command": "extension.previewAsJson", "title": "YAML+JSON: Preview as JSON (from YAML. Opens in new file)" + }, + { + "command": "extension.interactiveConverter", + "title": "YAML+JSON: Open Interactive Converter", + "icon": "$(arrow-swap)" } ], "menus": { @@ -130,6 +135,15 @@ { "command": "extension.previewAsJson", "when": "editorLangId == 'yaml'" + }, + { + "command": "extension.interactiveConverter" + } + ], + "editor/title": [ + { + "command": "extension.interactiveConverter", + "group": "navigation" } ], "explorer/context": [ diff --git a/src/extension.ts b/src/extension.ts index 234e104..89343de 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { import { onConvertSelection } from './onConvertSelection'; import { ConvertFromType } from './converter'; import { onPreviewSelection } from './onPreviewSelection'; +import { onInteractiveConverter } from './onInteractiveConverter'; import { ConfigId, getConfig } from './config'; const { registerCommand, executeCommand } = vscode.commands; @@ -49,6 +50,7 @@ export function activate(context: vscode.ExtensionContext) { registerCommand('extension.convertYamlSelectionsToJson', onConvertSelectedYamlFilesToJson), registerCommand('extension.previewAsYaml', onPreviewSelection(ConvertFromType.Json)), registerCommand('extension.previewAsJson', onPreviewSelection(ConvertFromType.Yaml)), + registerCommand('extension.interactiveConverter', onInteractiveConverter), ); vscode.workspace.onDidRenameFiles(onFileRename); diff --git a/src/onInteractiveConverter.ts b/src/onInteractiveConverter.ts new file mode 100644 index 0000000..a452804 --- /dev/null +++ b/src/onInteractiveConverter.ts @@ -0,0 +1,331 @@ +import { contextProvider } from './contextProvider'; +import { getYamlFromJson, getJsonFromYaml } from './helpers'; + +type Direction = 'json-to-yaml' | 'yaml-to-json'; + +let panel: import('vscode').WebviewPanel | undefined; + +/** Generates a random one-time token for the CSP header and matching script/style tags, + * so the browser only executes inline content from this render — not anything injected by a third party. */ +function generateNonce(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < 32; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +function detectDirection(input: string): Direction { + const trimmed = input.trim(); + try { + JSON.parse(trimmed); + return 'json-to-yaml'; + } catch { + return 'yaml-to-json'; + } +} + +function getEditorContent(editor: import('vscode').TextEditor): { input: string; direction: Direction } | null { + const langId = editor.document.languageId; + if (langId === 'json' || langId === 'jsonc') { + return { input: editor.document.getText(), direction: 'json-to-yaml' }; + } + if (langId === 'yaml') { + return { input: editor.document.getText(), direction: 'yaml-to-json' }; + } + const text = editor.document.getText(); + if (text) { + return { input: text, direction: detectDirection(text) }; + } + return null; +} + +export function onInteractiveConverter(): void { + const vscode = contextProvider.vscode; + + // Read editor before reveal() so we capture it while the file is still focused + const editorContent = vscode.window.activeTextEditor ? getEditorContent(vscode.window.activeTextEditor) : null; + + if (panel) { + panel.reveal(); + if (editorContent) { + panel.webview.postMessage({ type: 'init', input: editorContent.input, direction: editorContent.direction }); + } + return; + } + + const initialInput = editorContent?.input ?? ''; + const initialDirection: Direction = editorContent?.direction ?? 'json-to-yaml'; + + panel = vscode.window.createWebviewPanel('yamlPlusJsonConverter', 'YAML ↔ JSON Converter', vscode.ViewColumn.Beside, { + enableScripts: true, + retainContextWhenHidden: true, + }); + + panel.webview.html = getWebviewHtml(); + + panel.webview.onDidReceiveMessage( + async (msg: { type: string; input?: string; direction?: Direction; text?: string }) => { + if (!panel) return; + + switch (msg.type) { + case 'ready': + panel.webview.postMessage({ type: 'init', input: initialInput, direction: initialDirection }); + break; + + case 'convert': { + const { input = '', direction = 'json-to-yaml' } = msg; + try { + const output = direction === 'json-to-yaml' ? getYamlFromJson(input) : getJsonFromYaml(input); + panel.webview.postMessage({ type: 'result', output }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Conversion failed.'; + panel.webview.postMessage({ type: 'error', message: errorMessage }); + } + break; + } + + case 'copy': + if (msg.text !== undefined) { + void contextProvider.vscode.env.clipboard.writeText(msg.text); + } + break; + + case 'saveAs': { + if (msg.text === undefined) break; + const isJsonToYaml = msg.direction === 'json-to-yaml'; + const filename = isJsonToYaml ? 'output.yaml' : 'output.json'; + const workspaceUri = contextProvider.vscode.workspace.workspaceFolders?.[0]?.uri; + const uri = await contextProvider.vscode.window.showSaveDialog({ + ...(workspaceUri ? { defaultUri: contextProvider.vscode.Uri.joinPath(workspaceUri, filename) } : {}), + filters: isJsonToYaml ? { 'YAML files': ['yaml', 'yml'] } : { 'JSON files': ['json', 'jsonc'] }, + }); + if (uri) { + await contextProvider.vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(msg.text)); + } + break; + } + } + }, + ); + + panel.onDidDispose(() => { + panel = undefined; + }); +} + +function getWebviewHtml(): string { + const nonce = generateNonce(); + return ` + +
+ + + +