From 52eda1ce027252d4583c55e688af1e9b4e9415be Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:34:36 +0800 Subject: [PATCH 01/16] feat: add full extension manifest with commands, menus, keybindings, and settings - Add @toon-format/toon and jsonc-parser as dependencies - Register 4 commands: jsonToToon, toonToJson, convertToToonFile, convertToJsonFile - Add context menus for explorer (.json/.toon) and editor - Add keyboard shortcuts: Cmd+Alt+T (to TOON), Cmd+Alt+J (to JSON) - Register TOON language with .toon extension - Add user settings for encode/decode options and validation toggle - Declare workspace trust support - Bump version to 0.1.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 182 ++++++++++++++++++++++++++++++++++++++++++++++--- pnpm-lock.yaml | 12 ++++ 2 files changed, 183 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 1fc2c75..7abb8b8 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,27 @@ { "publisher": "toon-format", "name": "toon", - "displayName": "Token-Oriented Object Notation (TOON) Support", + "displayName": "TOON — JSON ↔ TOON Converter", "type": "module", - "version": "0.0.1", + "version": "0.1.0", "packageManager": "pnpm@10.23.0", - "description": "Visual Studio Code extension for TOON format support", + "description": "Bidirectional JSON/TOON conversion, syntax highlighting, and real-time validation for the TOON format", "license": "MIT", - "homepage": "https://github.com/toon-format/vscode#readme", + "homepage": "https://github.com/akumarpalo/vscode#readme", "repository": { "type": "git", - "url": "https://github.com/toon-format/vscode.git" + "url": "https://github.com/akumarpalo/vscode.git" }, "bugs": { - "url": "https://github.com/toon-format/vscode/issues" + "url": "https://github.com/akumarpalo/vscode/issues" }, "keywords": [ "toon", + "json", + "converter", "format", - "serialization" + "serialization", + "token-efficient" ], "categories": [ "Programming Languages", @@ -27,15 +30,168 @@ "main": "./dist/extension.mjs", "engines": { "vscode": "^1.95.0", - "node": ">=24.0.0" + "node": ">=18.0.0" + }, + "capabilities": { + "untrustedWorkspaces": { + "supported": true + } }, "contributes": { + "languages": [ + { + "id": "toon", + "aliases": [ + "TOON", + "toon" + ], + "extensions": [ + ".toon" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "toon", + "scopeName": "source.toon", + "path": "./syntaxes/toon.tmLanguage.json" + } + ], "commands": [ { - "command": "toon.helloWorld", - "title": "TOON: Hello World" + "command": "toon.jsonToToon", + "title": "TOON: Convert JSON to TOON" + }, + { + "command": "toon.toonToJson", + "title": "TOON: Convert TOON to JSON" + }, + { + "command": "toon.convertToToonFile", + "title": "TOON: Convert to TOON (Save As…)" + }, + { + "command": "toon.convertToJsonFile", + "title": "TOON: Convert to JSON (Save As…)" + } + ], + "menus": { + "commandPalette": [ + { + "command": "toon.jsonToToon", + "when": "editorLangId == json || editorLangId == jsonc" + }, + { + "command": "toon.toonToJson", + "when": "editorLangId == toon" + }, + { + "command": "toon.convertToToonFile", + "when": "editorLangId == json || editorLangId == jsonc" + }, + { + "command": "toon.convertToJsonFile", + "when": "editorLangId == toon" + } + ], + "explorer/context": [ + { + "command": "toon.convertToToonFile", + "when": "resourceExtname == .json", + "group": "7_toon" + }, + { + "command": "toon.convertToJsonFile", + "when": "resourceExtname == .toon", + "group": "7_toon" + } + ], + "editor/context": [ + { + "command": "toon.jsonToToon", + "when": "editorLangId == json || editorLangId == jsonc", + "group": "1_modification" + }, + { + "command": "toon.toonToJson", + "when": "editorLangId == toon", + "group": "1_modification" + } + ] + }, + "keybindings": [ + { + "command": "toon.jsonToToon", + "key": "ctrl+alt+t", + "mac": "cmd+alt+t", + "when": "editorTextFocus" + }, + { + "command": "toon.toonToJson", + "key": "ctrl+alt+j", + "mac": "cmd+alt+j", + "when": "editorTextFocus" + } + ], + "configuration": { + "title": "TOON", + "properties": { + "toon.encode.indent": { + "type": "number", + "default": 2, + "description": "Indentation size for TOON output" + }, + "toon.encode.delimiter": { + "type": "string", + "enum": [ + ",", + "|", + "\t" + ], + "default": ",", + "description": "Delimiter for tabular arrays in TOON output" + }, + "toon.encode.keyFolding": { + "type": "string", + "enum": [ + "off", + "safe" + ], + "default": "off", + "description": "Enable dotted key folding (e.g., a.b.c: value)" + }, + "toon.decode.strict": { + "type": "boolean", + "default": true, + "description": "Use strict mode when decoding TOON" + }, + "toon.decode.expandPaths": { + "type": "string", + "enum": [ + "off", + "safe" + ], + "default": "off", + "description": "Expand dotted keys into nested objects when decoding" + }, + "toon.json.indent": { + "type": "number", + "default": 2, + "description": "Indentation size for JSON output" + }, + "toon.openAfterConvert": { + "type": "boolean", + "default": true, + "description": "Open the converted file after saving" + }, + "toon.validation.enable": { + "type": "boolean", + "default": true, + "description": "Enable real-time TOON validation in the Problems panel" + } } - ] + } }, "scripts": { "build": "tsdown", @@ -48,6 +204,10 @@ "publish": "pnpm build && vsce publish", "vscode:prepublish": "pnpm build" }, + "dependencies": { + "@toon-format/toon": "^2.3.0", + "jsonc-parser": "^3.2.0" + }, "devDependencies": { "@antfu/eslint-config": "^6.2.0", "@types/node": "^24.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be394e0..73ad60c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,13 @@ settings: importers: .: + dependencies: + '@toon-format/toon': + specifier: ^2.3.0 + version: 2.3.0 + jsonc-parser: + specifier: ^3.2.0 + version: 3.3.1 devDependencies: '@antfu/eslint-config': specifier: ^6.2.0 @@ -479,6 +486,9 @@ packages: '@textlint/types@15.4.0': resolution: {integrity: sha512-ZMwJgw/xjxJufOD+IB7I2Enl9Si4Hxo04B76RwUZ5cKBKzOPcmd6WvGe2F7jqdgmTdGnfMU+Bo/joQrjPNIWqg==} + '@toon-format/toon@2.3.0': + resolution: {integrity: sha512-/Ew9etdRQKVMnm9fDaCG0JjyAOK/O7T0M97oum1aW4W+UR8ZhVVPBanIV7oWgHBiGlnVxV9M55PWQCHofDV07w==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3060,6 +3070,8 @@ snapshots: dependencies: '@textlint/ast-node-types': 15.4.0 + '@toon-format/toon@2.3.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 From 16aed372d6294f7bfc73b34758db9d35347ee719 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:36:37 +0800 Subject: [PATCH 02/16] feat: add conversion logic wrapping @toon-format/toon encode/decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jsonToToon: JSON.parse (or jsonc-parser for JSONC) → encode() → TOON string - toonToJson: decode() → JSON.stringify with configurable indent - ConversionResult type with success/failure discrimination - Error position extraction for both JSON (via offset) and TOON (via ToonDecodeError.line) - Utility functions for reading user settings (encode/decode options) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/converter.ts | 105 +++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 27 ++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/converter.ts create mode 100644 src/utils.ts diff --git a/src/converter.ts b/src/converter.ts new file mode 100644 index 0000000..80d1f7b --- /dev/null +++ b/src/converter.ts @@ -0,0 +1,105 @@ +import { decode, encode } from '@toon-format/toon' +import type { DecodeOptions, EncodeOptions } from '@toon-format/toon' +import { parse as parseJsonc } from 'jsonc-parser' +import type { ParseError } from 'jsonc-parser' + +export interface ConversionSuccess { + success: true + output: string +} + +export interface ConversionFailure { + success: false + error: string + line?: number +} + +export type ConversionResult = ConversionSuccess | ConversionFailure + +export interface JsonToToonOptions { + indent?: number + delimiter?: ',' | '|' | '\t' + keyFolding?: 'off' | 'safe' +} + +export interface ToonToJsonOptions { + indent?: number + strict?: boolean + expandPaths?: 'off' | 'safe' +} + +export function jsonToToon(input: string, languageId: string, options: JsonToToonOptions = {}): ConversionResult { + let data: unknown + + try { + if (languageId === 'jsonc') { + const errors: ParseError[] = [] + data = parseJsonc(input, errors, { allowTrailingComma: true }) + if (errors.length > 0) { + const firstError = errors[0]! + const line = offsetToLine(input, firstError.offset) + return { success: false, error: `JSONC parse error at offset ${firstError.offset}`, line } + } + } + else { + data = JSON.parse(input) + } + } + catch (e) { + const line = extractJsonErrorLine(e as SyntaxError, input) + return { success: false, error: (e as Error).message, line } + } + + try { + const encodeOptions: EncodeOptions = {} + if (options.indent !== undefined) + encodeOptions.indent = options.indent + if (options.delimiter !== undefined) + encodeOptions.delimiter = options.delimiter + if (options.keyFolding !== undefined) + encodeOptions.keyFolding = options.keyFolding + + const output = encode(data, encodeOptions) + return { success: true, output } + } + catch (e) { + return { success: false, error: `Encode error: ${(e as Error).message}` } + } +} + +export function toonToJson(input: string, options: ToonToJsonOptions = {}): ConversionResult { + try { + const decodeOptions: DecodeOptions = {} + if (options.strict !== undefined) + decodeOptions.strict = options.strict + if (options.expandPaths !== undefined) + decodeOptions.expandPaths = options.expandPaths + + const data = decode(input, decodeOptions) + const indent = options.indent ?? 2 + const output = JSON.stringify(data, null, indent) + return { success: true, output } + } + catch (e) { + const line = (e as { line?: number }).line + return { success: false, error: (e as Error).message, line } + } +} + +function offsetToLine(text: string, offset: number): number { + let line = 1 + for (let i = 0; i < offset && i < text.length; i++) { + if (text[i] === '\n') + line++ + } + return line +} + +function extractJsonErrorLine(error: SyntaxError, input: string): number | undefined { + const match = error.message.match(/position\s+(\d+)/) + if (match) { + const offset = Number.parseInt(match[1]!, 10) + return offsetToLine(input, offset) + } + return undefined +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..783e705 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode' +import type { JsonToToonOptions, ToonToJsonOptions } from './converter' + +export function getEncodeOptions(): JsonToToonOptions { + const config = vscode.workspace.getConfiguration('toon') + return { + indent: config.get('encode.indent', 2), + delimiter: config.get<',' | '|' | '\t'>('encode.delimiter', ','), + keyFolding: config.get<'off' | 'safe'>('encode.keyFolding', 'off'), + } +} + +export function getDecodeOptions(): ToonToJsonOptions { + const config = vscode.workspace.getConfiguration('toon') + return { + indent: config.get('json.indent', 2), + strict: config.get('decode.strict', true), + expandPaths: config.get<'off' | 'safe'>('decode.expandPaths', 'off'), + } +} + +export function getFullDocumentRange(document: vscode.TextDocument): vscode.Range { + return new vscode.Range( + document.positionAt(0), + document.positionAt(document.getText().length), + ) +} From d57e5a18792d8cfcc399183b6e10d9d927c7862c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:38:10 +0800 Subject: [PATCH 03/16] feat: add command handlers for all 4 conversion commands - jsonToToon: in-editor conversion with selection support and language mode switch - toonToJson: in-editor conversion with selection support and language mode switch - convertToToonFile: explorer/file conversion with save dialog - convertToJsonFile: explorer/file conversion with save dialog - Safety net guards prevent converting to the same format - Error handling with cursor jump to error line - Status bar feedback on success (3s transient message) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/convertToJsonFile.ts | 47 +++++++++++++++++++++++++ src/commands/convertToToonFile.ts | 48 ++++++++++++++++++++++++++ src/commands/index.ts | 4 +++ src/commands/jsonToToon.ts | 57 +++++++++++++++++++++++++++++++ src/commands/toonToJson.ts | 57 +++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 src/commands/convertToJsonFile.ts create mode 100644 src/commands/convertToToonFile.ts create mode 100644 src/commands/index.ts create mode 100644 src/commands/jsonToToon.ts create mode 100644 src/commands/toonToJson.ts diff --git a/src/commands/convertToJsonFile.ts b/src/commands/convertToJsonFile.ts new file mode 100644 index 0000000..b32f70b --- /dev/null +++ b/src/commands/convertToJsonFile.ts @@ -0,0 +1,47 @@ +import * as vscode from 'vscode' +import { toonToJson } from '../converter' +import { getDecodeOptions } from '../utils' + +export async function convertToJsonFileCommand(uri?: vscode.Uri): Promise { + const sourceUri = uri ?? vscode.window.activeTextEditor?.document.uri + if (!sourceUri) + return + + const fileContent = await vscode.workspace.fs.readFile(sourceUri) + const text = new TextDecoder('utf-8').decode(fileContent) + + if (!text.trim()) { + vscode.window.showInformationMessage('Nothing to convert — file is empty.') + return + } + + const options = getDecodeOptions() + const result = toonToJson(text, options) + + if (!result.success) { + vscode.window.showErrorMessage(`Conversion failed: ${result.error}`) + return + } + + const defaultPath = sourceUri.fsPath.replace(/\.toon$/, '.json') + const defaultUri = vscode.Uri.file(defaultPath) + + const targetUri = await vscode.window.showSaveDialog({ + defaultUri, + filters: { 'JSON files': ['json'] }, + title: 'Save JSON file as…', + }) + + if (!targetUri) + return + + await vscode.workspace.fs.writeFile(targetUri, new TextEncoder().encode(result.output)) + + const config = vscode.workspace.getConfiguration('toon') + if (config.get('openAfterConvert', true)) { + const doc = await vscode.workspace.openTextDocument(targetUri) + await vscode.window.showTextDocument(doc) + } + + vscode.window.setStatusBarMessage('$(check) Saved JSON file', 3000) +} diff --git a/src/commands/convertToToonFile.ts b/src/commands/convertToToonFile.ts new file mode 100644 index 0000000..cc913da --- /dev/null +++ b/src/commands/convertToToonFile.ts @@ -0,0 +1,48 @@ +import * as vscode from 'vscode' +import { jsonToToon } from '../converter' +import { getEncodeOptions } from '../utils' + +export async function convertToToonFileCommand(uri?: vscode.Uri): Promise { + const sourceUri = uri ?? vscode.window.activeTextEditor?.document.uri + if (!sourceUri) + return + + const fileContent = await vscode.workspace.fs.readFile(sourceUri) + const text = new TextDecoder('utf-8').decode(fileContent) + + if (!text.trim()) { + vscode.window.showInformationMessage('Nothing to convert — file is empty.') + return + } + + const languageId = sourceUri.fsPath.endsWith('.jsonc') ? 'jsonc' : 'json' + const options = getEncodeOptions() + const result = jsonToToon(text, languageId, options) + + if (!result.success) { + vscode.window.showErrorMessage(`Conversion failed: ${result.error}`) + return + } + + const defaultPath = sourceUri.fsPath.replace(/\.jsonc?$/, '.toon') + const defaultUri = vscode.Uri.file(defaultPath) + + const targetUri = await vscode.window.showSaveDialog({ + defaultUri, + filters: { 'TOON files': ['toon'] }, + title: 'Save TOON file as…', + }) + + if (!targetUri) + return + + await vscode.workspace.fs.writeFile(targetUri, new TextEncoder().encode(result.output)) + + const config = vscode.workspace.getConfiguration('toon') + if (config.get('openAfterConvert', true)) { + const doc = await vscode.workspace.openTextDocument(targetUri) + await vscode.window.showTextDocument(doc) + } + + vscode.window.setStatusBarMessage('$(check) Saved TOON file', 3000) +} diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..021d3fa --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,4 @@ +export { jsonToToonCommand } from './jsonToToon' +export { toonToJsonCommand } from './toonToJson' +export { convertToToonFileCommand } from './convertToToonFile' +export { convertToJsonFileCommand } from './convertToJsonFile' diff --git a/src/commands/jsonToToon.ts b/src/commands/jsonToToon.ts new file mode 100644 index 0000000..69f2cf2 --- /dev/null +++ b/src/commands/jsonToToon.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode' +import { jsonToToon } from '../converter' +import { getEncodeOptions, getFullDocumentRange } from '../utils' + +export async function jsonToToonCommand(): Promise { + const editor = vscode.window.activeTextEditor + if (!editor) + return + + const document = editor.document + const languageId = document.languageId + + if (languageId === 'toon') { + vscode.window.showInformationMessage('This file is already TOON.') + return + } + + if (languageId !== 'json' && languageId !== 'jsonc') { + vscode.window.showInformationMessage('This command works on JSON or JSONC files.') + return + } + + const selection = editor.selection + const range = selection.isEmpty + ? getFullDocumentRange(document) + : new vscode.Range(selection.start, selection.end) + + const text = document.getText(range) + + if (!text.trim()) { + vscode.window.showInformationMessage('Nothing to convert — selection is empty.') + return + } + + const options = getEncodeOptions() + const result = jsonToToon(text, languageId, options) + + if (!result.success) { + vscode.window.showErrorMessage(`Conversion failed: ${result.error}`) + if (result.line !== undefined) { + const pos = new vscode.Position(range.start.line + result.line - 1, 0) + editor.selection = new vscode.Selection(pos, pos) + editor.revealRange(new vscode.Range(pos, pos)) + } + return + } + + await editor.edit(editBuilder => { + editBuilder.replace(range, result.output) + }) + + if (selection.isEmpty) { + await vscode.languages.setTextDocumentLanguage(document, 'toon') + } + + vscode.window.setStatusBarMessage('$(check) Converted to TOON', 3000) +} diff --git a/src/commands/toonToJson.ts b/src/commands/toonToJson.ts new file mode 100644 index 0000000..50a1ed8 --- /dev/null +++ b/src/commands/toonToJson.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode' +import { toonToJson } from '../converter' +import { getDecodeOptions, getFullDocumentRange } from '../utils' + +export async function toonToJsonCommand(): Promise { + const editor = vscode.window.activeTextEditor + if (!editor) + return + + const document = editor.document + const languageId = document.languageId + + if (languageId === 'json' || languageId === 'jsonc') { + vscode.window.showInformationMessage('This file is already JSON.') + return + } + + if (languageId !== 'toon') { + vscode.window.showInformationMessage('This command works on TOON files.') + return + } + + const selection = editor.selection + const range = selection.isEmpty + ? getFullDocumentRange(document) + : new vscode.Range(selection.start, selection.end) + + const text = document.getText(range) + + if (!text.trim()) { + vscode.window.showInformationMessage('Nothing to convert — selection is empty.') + return + } + + const options = getDecodeOptions() + const result = toonToJson(text, options) + + if (!result.success) { + vscode.window.showErrorMessage(`Conversion failed: ${result.error}`) + if (result.line !== undefined) { + const pos = new vscode.Position(range.start.line + result.line - 1, 0) + editor.selection = new vscode.Selection(pos, pos) + editor.revealRange(new vscode.Range(pos, pos)) + } + return + } + + await editor.edit(editBuilder => { + editBuilder.replace(range, result.output) + }) + + if (selection.isEmpty) { + await vscode.languages.setTextDocumentLanguage(document, 'json') + } + + vscode.window.setStatusBarMessage('$(check) Converted to JSON', 3000) +} From ce6c8458bbbdc0353b4bc90d5461ea11f7eefc89 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:39:42 +0800 Subject: [PATCH 04/16] feat: add real-time TOON validation and wire up extension entry point - DiagnosticCollection validates .toon files in the Problems panel - Validates on open, change (debounced 300ms), and clears on close - Respects toon.validation.enable setting - Extension entry point registers all 4 commands with dispose pattern - Sets up diagnostics on activation Co-Authored-By: Claude Opus 4.6 (1M context) --- src/diagnostics.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 23 +++++++++------ 2 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/diagnostics.ts diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..5ca74fc --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode' +import { decode } from '@toon-format/toon' + +let diagnosticCollection: vscode.DiagnosticCollection +let debounceTimer: ReturnType | undefined + +export function setupDiagnostics(context: vscode.ExtensionContext): void { + diagnosticCollection = vscode.languages.createDiagnosticCollection('toon') + context.subscriptions.push(diagnosticCollection) + + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(onDocumentEvent), + vscode.workspace.onDidChangeTextDocument((e) => { + onDocumentEventDebounced(e.document) + }), + vscode.workspace.onDidCloseTextDocument((document) => { + diagnosticCollection.delete(document.uri) + }), + ) + + vscode.workspace.textDocuments.forEach(onDocumentEvent) +} + +function onDocumentEvent(document: vscode.TextDocument): void { + validateToonDocument(document) +} + +function onDocumentEventDebounced(document: vscode.TextDocument): void { + if (debounceTimer) + clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => validateToonDocument(document), 300) +} + +function validateToonDocument(document: vscode.TextDocument): void { + if (document.languageId !== 'toon') { + return + } + + const config = vscode.workspace.getConfiguration('toon') + if (!config.get('validation.enable', true)) { + diagnosticCollection.set(document.uri, []) + return + } + + const text = document.getText() + if (!text.trim()) { + diagnosticCollection.set(document.uri, []) + return + } + + try { + decode(text, { strict: true }) + diagnosticCollection.set(document.uri, []) + } + catch (e) { + const err = e as Error & { line?: number } + const lineNum = (err.line ?? 1) - 1 + const lineText = document.lineAt(Math.min(lineNum, document.lineCount - 1)) + const range = new vscode.Range(lineText.range.start, lineText.range.end) + + const diagnostic = new vscode.Diagnostic( + range, + err.message, + vscode.DiagnosticSeverity.Error, + ) + diagnostic.source = 'TOON' + + diagnosticCollection.set(document.uri, [diagnostic]) + } +} diff --git a/src/extension.ts b/src/extension.ts index 7c976c7..002ea70 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,14 +1,21 @@ import * as vscode from 'vscode' +import { + convertToJsonFileCommand, + convertToToonFileCommand, + jsonToToonCommand, + toonToJsonCommand, +} from './commands' +import { setupDiagnostics } from './diagnostics' export function activate(context: vscode.ExtensionContext): void { - // Register the hello world command - const disposable = vscode.commands.registerCommand('toon.helloWorld', () => { - vscode.window.showInformationMessage('Hello World from TOON!') - }) + context.subscriptions.push( + vscode.commands.registerCommand('toon.jsonToToon', jsonToToonCommand), + vscode.commands.registerCommand('toon.toonToJson', toonToJsonCommand), + vscode.commands.registerCommand('toon.convertToToonFile', convertToToonFileCommand), + vscode.commands.registerCommand('toon.convertToJsonFile', convertToJsonFileCommand), + ) - context.subscriptions.push(disposable) + setupDiagnostics(context) } -export function deactivate(): void { - // Cleanup if needed -} +export function deactivate(): void {} From 3cb0dcb436b1e62d1a7ff029c183786285e2dee3 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:43:54 +0800 Subject: [PATCH 05/16] feat: add TextMate grammar and language configuration for .toon files - Syntax highlighting for keys, values, arrays, tabular headers, escape sequences - Scopes: support.type.property-name, string.quoted.double, constant.numeric, constant.language.boolean, constant.language.null, variable.other.field - Array header regex matches key[N]{fields}: patterns - Language config: bracket pairs, auto-close, off-side folding, indentation rules Co-Authored-By: Claude Opus 4.6 (1M context) --- language-configuration.json | 24 ++++++ syntaxes/toon.tmLanguage.json | 140 ++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 language-configuration.json create mode 100644 syntaxes/toon.tmLanguage.json diff --git a/language-configuration.json b/language-configuration.json new file mode 100644 index 0000000..9c059c9 --- /dev/null +++ b/language-configuration.json @@ -0,0 +1,24 @@ +{ + "brackets": [ + ["[", "]"], + ["{", "}"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "\"", "close": "\"", "notIn": ["string"] } + ], + "surroundingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "\"", "close": "\"" } + ], + "folding": { + "offSide": true + }, + "indentationRules": { + "increaseIndentPattern": "^.+:\\s*$", + "decreaseIndentPattern": "^\\s*$" + }, + "wordPattern": "[^\\s,:\\[\\]{}\"']+" +} diff --git a/syntaxes/toon.tmLanguage.json b/syntaxes/toon.tmLanguage.json new file mode 100644 index 0000000..b624434 --- /dev/null +++ b/syntaxes/toon.tmLanguage.json @@ -0,0 +1,140 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "TOON", + "scopeName": "source.toon", + "patterns": [ + { "include": "#list-item" }, + { "include": "#array-header-line" }, + { "include": "#key-value-line" }, + { "include": "#key-value-line-simple" }, + { "include": "#key-nested" }, + { "include": "#tabular-row" } + ], + "repository": { + "list-item": { + "match": "^(\\s*)(-)\\s", + "captures": { + "2": { "name": "punctuation.definition.list.begin.toon" } + } + }, + "array-header-line": { + "comment": "Matches: key[N]{fields}: (end of line, nested content below)", + "match": "^(\\s*)([\\w][\\w.]*)(\\[)(\\d+)([|\\t])?(\\])(?:(\\{)([^}]*)(\\}))?(:)\\s*$", + "captures": { + "2": { "name": "support.type.property-name.toon" }, + "3": { "name": "punctuation.definition.array.begin.toon" }, + "4": { "name": "constant.numeric.toon" }, + "5": { "name": "punctuation.separator.delimiter.toon" }, + "6": { "name": "punctuation.definition.array.end.toon" }, + "7": { "name": "punctuation.definition.fieldlist.begin.toon" }, + "8": { "name": "variable.other.field.toon" }, + "9": { "name": "punctuation.definition.fieldlist.end.toon" }, + "10": { "name": "punctuation.separator.key-value.toon" } + } + }, + "key-value-line": { + "comment": "Matches: key[N]{fields}: inline-values (primitive array with header)", + "match": "^(\\s*)([\\w][\\w.]*)(\\[)(\\d+)([|\\t])?(\\])(?:(\\{)([^}]*)(\\}))?(:)\\s+(.+)$", + "captures": { + "2": { "name": "support.type.property-name.toon" }, + "3": { "name": "punctuation.definition.array.begin.toon" }, + "4": { "name": "constant.numeric.toon" }, + "5": { "name": "punctuation.separator.delimiter.toon" }, + "6": { "name": "punctuation.definition.array.end.toon" }, + "7": { "name": "punctuation.definition.fieldlist.begin.toon" }, + "8": { "name": "variable.other.field.toon" }, + "9": { "name": "punctuation.definition.fieldlist.end.toon" }, + "10": { "name": "punctuation.separator.key-value.toon" }, + "11": { "patterns": [{ "include": "#inline-values" }] } + } + }, + "key-value-line-simple": { + "comment": "Matches: key: value", + "match": "^(\\s*)([\\w][\\w.]*)(:)\\s+(.+)$", + "captures": { + "2": { "name": "support.type.property-name.toon" }, + "3": { "name": "punctuation.separator.key-value.toon" }, + "4": { "patterns": [{ "include": "#value" }] } + } + }, + "key-nested": { + "comment": "Matches: key: (end of line, nested content below)", + "match": "^(\\s*)([\\w][\\w.]*)(:)\\s*$", + "captures": { + "2": { "name": "support.type.property-name.toon" }, + "3": { "name": "punctuation.separator.key-value.toon" } + } + }, + "inline-values": { + "patterns": [ + { "include": "#string-quoted" }, + { "include": "#constant-boolean" }, + { "include": "#constant-null" }, + { "include": "#constant-numeric" }, + { "include": "#punctuation-delimiter" } + ] + }, + "tabular-row": { + "comment": "Catch-all for lines that are tabular data rows (values separated by delimiters)", + "patterns": [ + { "include": "#string-quoted" }, + { "include": "#constant-boolean" }, + { "include": "#constant-null" }, + { "include": "#constant-numeric" }, + { "include": "#punctuation-delimiter" } + ] + }, + "value": { + "patterns": [ + { "include": "#string-quoted" }, + { "include": "#constant-boolean" }, + { "include": "#constant-null" }, + { "include": "#constant-numeric" }, + { "include": "#empty-array" }, + { "include": "#string-unquoted" } + ] + }, + "string-quoted": { + "name": "string.quoted.double.toon", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { "name": "punctuation.definition.string.begin.toon" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.string.end.toon" } + }, + "patterns": [ + { "include": "#escape-sequence" } + ] + }, + "escape-sequence": { + "name": "constant.character.escape.toon", + "match": "\\\\(?:[\"\\\\/nrt]|u[0-9a-fA-F]{4})" + }, + "constant-boolean": { + "name": "constant.language.boolean.toon", + "match": "\\b(true|false)\\b" + }, + "constant-null": { + "name": "constant.language.null.toon", + "match": "\\bnull\\b" + }, + "constant-numeric": { + "name": "constant.numeric.toon", + "match": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?(?=\\s*[,|\\t]|\\s*$)" + }, + "empty-array": { + "name": "constant.language.toon", + "match": "\\[\\]" + }, + "punctuation-delimiter": { + "name": "punctuation.separator.toon", + "match": "[,|]" + }, + "string-unquoted": { + "name": "string.unquoted.toon", + "match": "[^,|\\s\"][^,|]*" + } + } +} From de2b0e793a0685843794a610bfc10308b8d3b780 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 19:45:50 +0800 Subject: [PATCH 06/16] chore: update build config and .vscodeignore - Disable dts generation in tsdown (not needed for extension) - Enable clean build output - Add .node-version (22) for development tooling - Update .vscodeignore to exclude dev files, keep dist/, syntaxes/, language-configuration.json Co-Authored-By: Claude Opus 4.6 (1M context) --- .node-version | 1 + .vscodeignore | 8 ++++++-- tsdown.config.ts | 8 +++----- 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/.vscodeignore b/.vscodeignore index 6de55ed..fe6f377 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,6 +8,10 @@ src/ .gitignore eslint.config.mjs pnpm-lock.yaml +pnpm-workspace.yaml tsconfig.json -*.md -!README.md +tsdown.config.ts +CONTRIBUTING.md +**/*.map +**/*.ts +!dist/** diff --git a/tsdown.config.ts b/tsdown.config.ts index 19a93de..24ff907 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,10 +1,8 @@ -import type { UserConfig, UserConfigFn } from 'tsdown/config' import { defineConfig } from 'tsdown/config' -const config: UserConfig | UserConfigFn = defineConfig({ +export default defineConfig({ entry: 'src/extension.ts', external: ['vscode'], - dts: true, + dts: false, + clean: true, }) - -export default config From 24bb14713daed21875c08054594a46d5526ace00 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 20:04:26 +0800 Subject: [PATCH 07/16] chore: add launch.json for extension debugging Adds "Run Extension" configuration that launches the Extension Development Host with a pre-build step. Co-Authored-By: Claude Opus 4.6 (1M context) --- .vscode/launch.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fcda722 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.mjs" + ], + "preLaunchTask": "npm: build" + } + ] +} From 603193a90e9f4febe30bc6e4fba19f742568a88d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 20:15:29 +0800 Subject: [PATCH 08/16] fix: allow selection-based conversion in any file type Previously, commands rejected files that weren't .json/.toon. Now: if text is selected, attempt conversion regardless of file type. Language checks only apply for full-document conversion (no selection). This enables converting JSON/TOON snippets embedded in .md, .txt, or any other file by selecting the relevant text first. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 4 ++-- src/commands/jsonToToon.ts | 31 +++++++++++++++++-------------- src/commands/toonToJson.ts | 28 +++++++++++++++------------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 7abb8b8..d3ac87b 100644 --- a/package.json +++ b/package.json @@ -80,11 +80,11 @@ "commandPalette": [ { "command": "toon.jsonToToon", - "when": "editorLangId == json || editorLangId == jsonc" + "when": "editorTextFocus" }, { "command": "toon.toonToJson", - "when": "editorLangId == toon" + "when": "editorTextFocus" }, { "command": "toon.convertToToonFile", diff --git a/src/commands/jsonToToon.ts b/src/commands/jsonToToon.ts index 69f2cf2..9d47597 100644 --- a/src/commands/jsonToToon.ts +++ b/src/commands/jsonToToon.ts @@ -9,21 +9,23 @@ export async function jsonToToonCommand(): Promise { const document = editor.document const languageId = document.languageId + const selection = editor.selection + const hasSelection = !selection.isEmpty - if (languageId === 'toon') { - vscode.window.showInformationMessage('This file is already TOON.') - return - } - - if (languageId !== 'json' && languageId !== 'jsonc') { - vscode.window.showInformationMessage('This command works on JSON or JSONC files.') - return + if (!hasSelection) { + if (languageId === 'toon') { + vscode.window.showInformationMessage('This file is already TOON.') + return + } + if (languageId !== 'json' && languageId !== 'jsonc') { + vscode.window.showInformationMessage('Select JSON text to convert, or open a .json file.') + return + } } - const selection = editor.selection - const range = selection.isEmpty - ? getFullDocumentRange(document) - : new vscode.Range(selection.start, selection.end) + const range = hasSelection + ? new vscode.Range(selection.start, selection.end) + : getFullDocumentRange(document) const text = document.getText(range) @@ -32,8 +34,9 @@ export async function jsonToToonCommand(): Promise { return } + const parseLanguageId = (languageId === 'json' || languageId === 'jsonc') ? languageId : 'json' const options = getEncodeOptions() - const result = jsonToToon(text, languageId, options) + const result = jsonToToon(text, parseLanguageId, options) if (!result.success) { vscode.window.showErrorMessage(`Conversion failed: ${result.error}`) @@ -49,7 +52,7 @@ export async function jsonToToonCommand(): Promise { editBuilder.replace(range, result.output) }) - if (selection.isEmpty) { + if (!hasSelection) { await vscode.languages.setTextDocumentLanguage(document, 'toon') } diff --git a/src/commands/toonToJson.ts b/src/commands/toonToJson.ts index 50a1ed8..84bfc47 100644 --- a/src/commands/toonToJson.ts +++ b/src/commands/toonToJson.ts @@ -9,21 +9,23 @@ export async function toonToJsonCommand(): Promise { const document = editor.document const languageId = document.languageId + const selection = editor.selection + const hasSelection = !selection.isEmpty - if (languageId === 'json' || languageId === 'jsonc') { - vscode.window.showInformationMessage('This file is already JSON.') - return - } - - if (languageId !== 'toon') { - vscode.window.showInformationMessage('This command works on TOON files.') - return + if (!hasSelection) { + if (languageId === 'json' || languageId === 'jsonc') { + vscode.window.showInformationMessage('This file is already JSON.') + return + } + if (languageId !== 'toon') { + vscode.window.showInformationMessage('Select TOON text to convert, or open a .toon file.') + return + } } - const selection = editor.selection - const range = selection.isEmpty - ? getFullDocumentRange(document) - : new vscode.Range(selection.start, selection.end) + const range = hasSelection + ? new vscode.Range(selection.start, selection.end) + : getFullDocumentRange(document) const text = document.getText(range) @@ -49,7 +51,7 @@ export async function toonToJsonCommand(): Promise { editBuilder.replace(range, result.output) }) - if (selection.isEmpty) { + if (!hasSelection) { await vscode.languages.setTextDocumentLanguage(document, 'json') } From de11def346fa147365764f89c07fa142ace7a8dd Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 3 Jun 2026 20:18:03 +0800 Subject: [PATCH 09/16] fix: show conversion commands in context menu when text is selected Editor context menu now shows "Convert JSON to TOON" and "Convert TOON to JSON" when there's an active selection, regardless of the file's language. This enables converting embedded JSON/TOON in any file type via right-click. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d3ac87b..b495854 100644 --- a/package.json +++ b/package.json @@ -110,12 +110,12 @@ "editor/context": [ { "command": "toon.jsonToToon", - "when": "editorLangId == json || editorLangId == jsonc", + "when": "editorLangId == json || editorLangId == jsonc || editorHasSelection", "group": "1_modification" }, { "command": "toon.toonToJson", - "when": "editorLangId == toon", + "when": "editorLangId == toon || editorHasSelection", "group": "1_modification" } ] From ff0d7c7f359411b467304b97cec145cfc0fc1451 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 10:31:05 +0800 Subject: [PATCH 10/16] chore: remove TOON: prefix from command titles Cleaner context menu labels without the redundant prefix. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b495854..2f7fe48 100644 --- a/package.json +++ b/package.json @@ -61,19 +61,19 @@ "commands": [ { "command": "toon.jsonToToon", - "title": "TOON: Convert JSON to TOON" + "title": "Convert JSON to TOON" }, { "command": "toon.toonToJson", - "title": "TOON: Convert TOON to JSON" + "title": "Convert TOON to JSON" }, { "command": "toon.convertToToonFile", - "title": "TOON: Convert to TOON (Save As…)" + "title": "Convert to TOON (Save As…)" }, { "command": "toon.convertToJsonFile", - "title": "TOON: Convert to JSON (Save As…)" + "title": "Convert to JSON (Save As…)" } ], "menus": { From 68aa7e920e800bf44e985dbef0dce14d7f770f2c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 11:34:12 +0800 Subject: [PATCH 11/16] fix: address code review findings - Fix debounce race condition: use per-document timer map instead of single global timer. Timers cleaned up on document close. - Improve JSONC error messages: use printParseErrorCode() for human- readable error types (e.g., "CommaExpected at line 5") - Add V8-specific comment on extractJsonErrorLine regex - Add try/catch around workspace.fs.readFile in file conversion commands to handle permission/missing file errors gracefully - Check editor.edit() return value: show error if edit was rejected - Remove toon.decode.strict setting: always decode in strict mode (fixes diagnostics vs command inconsistency) Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 5 ----- src/commands/convertToJsonFile.ts | 11 ++++++++-- src/commands/convertToToonFile.ts | 11 ++++++++-- src/commands/jsonToToon.ts | 7 +++++- src/commands/toonToJson.ts | 7 +++++- src/converter.ts | 11 +++++----- src/diagnostics.ts | 36 ++++++++++++++++++++----------- src/utils.ts | 1 - 8 files changed, 58 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 2f7fe48..844bd86 100644 --- a/package.json +++ b/package.json @@ -161,11 +161,6 @@ "default": "off", "description": "Enable dotted key folding (e.g., a.b.c: value)" }, - "toon.decode.strict": { - "type": "boolean", - "default": true, - "description": "Use strict mode when decoding TOON" - }, "toon.decode.expandPaths": { "type": "string", "enum": [ diff --git a/src/commands/convertToJsonFile.ts b/src/commands/convertToJsonFile.ts index b32f70b..c565a2d 100644 --- a/src/commands/convertToJsonFile.ts +++ b/src/commands/convertToJsonFile.ts @@ -7,8 +7,15 @@ export async function convertToJsonFileCommand(uri?: vscode.Uri): Promise if (!sourceUri) return - const fileContent = await vscode.workspace.fs.readFile(sourceUri) - const text = new TextDecoder('utf-8').decode(fileContent) + let text: string + try { + const fileContent = await vscode.workspace.fs.readFile(sourceUri) + text = new TextDecoder('utf-8').decode(fileContent) + } + catch (e) { + vscode.window.showErrorMessage(`Failed to read file: ${(e as Error).message}`) + return + } if (!text.trim()) { vscode.window.showInformationMessage('Nothing to convert — file is empty.') diff --git a/src/commands/convertToToonFile.ts b/src/commands/convertToToonFile.ts index cc913da..f11cbca 100644 --- a/src/commands/convertToToonFile.ts +++ b/src/commands/convertToToonFile.ts @@ -7,8 +7,15 @@ export async function convertToToonFileCommand(uri?: vscode.Uri): Promise if (!sourceUri) return - const fileContent = await vscode.workspace.fs.readFile(sourceUri) - const text = new TextDecoder('utf-8').decode(fileContent) + let text: string + try { + const fileContent = await vscode.workspace.fs.readFile(sourceUri) + text = new TextDecoder('utf-8').decode(fileContent) + } + catch (e) { + vscode.window.showErrorMessage(`Failed to read file: ${(e as Error).message}`) + return + } if (!text.trim()) { vscode.window.showInformationMessage('Nothing to convert — file is empty.') diff --git a/src/commands/jsonToToon.ts b/src/commands/jsonToToon.ts index 9d47597..b1155a8 100644 --- a/src/commands/jsonToToon.ts +++ b/src/commands/jsonToToon.ts @@ -48,10 +48,15 @@ export async function jsonToToonCommand(): Promise { return } - await editor.edit(editBuilder => { + const applied = await editor.edit(editBuilder => { editBuilder.replace(range, result.output) }) + if (!applied) { + vscode.window.showErrorMessage('Failed to apply edit — the document may have changed.') + return + } + if (!hasSelection) { await vscode.languages.setTextDocumentLanguage(document, 'toon') } diff --git a/src/commands/toonToJson.ts b/src/commands/toonToJson.ts index 84bfc47..c072509 100644 --- a/src/commands/toonToJson.ts +++ b/src/commands/toonToJson.ts @@ -47,10 +47,15 @@ export async function toonToJsonCommand(): Promise { return } - await editor.edit(editBuilder => { + const applied = await editor.edit(editBuilder => { editBuilder.replace(range, result.output) }) + if (!applied) { + vscode.window.showErrorMessage('Failed to apply edit — the document may have changed.') + return + } + if (!hasSelection) { await vscode.languages.setTextDocumentLanguage(document, 'json') } diff --git a/src/converter.ts b/src/converter.ts index 80d1f7b..8eb1f4e 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -1,6 +1,6 @@ import { decode, encode } from '@toon-format/toon' import type { DecodeOptions, EncodeOptions } from '@toon-format/toon' -import { parse as parseJsonc } from 'jsonc-parser' +import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser' import type { ParseError } from 'jsonc-parser' export interface ConversionSuccess { @@ -24,7 +24,6 @@ export interface JsonToToonOptions { export interface ToonToJsonOptions { indent?: number - strict?: boolean expandPaths?: 'off' | 'safe' } @@ -38,7 +37,8 @@ export function jsonToToon(input: string, languageId: string, options: JsonToToo if (errors.length > 0) { const firstError = errors[0]! const line = offsetToLine(input, firstError.offset) - return { success: false, error: `JSONC parse error at offset ${firstError.offset}`, line } + const errorType = printParseErrorCode(firstError.error) + return { success: false, error: `${errorType} at line ${line}`, line } } } else { @@ -69,9 +69,7 @@ export function jsonToToon(input: string, languageId: string, options: JsonToToo export function toonToJson(input: string, options: ToonToJsonOptions = {}): ConversionResult { try { - const decodeOptions: DecodeOptions = {} - if (options.strict !== undefined) - decodeOptions.strict = options.strict + const decodeOptions: DecodeOptions = { strict: true } if (options.expandPaths !== undefined) decodeOptions.expandPaths = options.expandPaths @@ -95,6 +93,7 @@ function offsetToLine(text: string, offset: number): number { return line } +// V8-specific: matches "at position N" format. Safe because VS Code always runs on V8. function extractJsonErrorLine(error: SyntaxError, input: string): number | undefined { const match = error.message.match(/position\s+(\d+)/) if (match) { diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 5ca74fc..7c64b69 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -2,39 +2,49 @@ import * as vscode from 'vscode' import { decode } from '@toon-format/toon' let diagnosticCollection: vscode.DiagnosticCollection -let debounceTimer: ReturnType | undefined +const debounceTimers = new Map>() export function setupDiagnostics(context: vscode.ExtensionContext): void { diagnosticCollection = vscode.languages.createDiagnosticCollection('toon') context.subscriptions.push(diagnosticCollection) context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(onDocumentEvent), + vscode.workspace.onDidOpenTextDocument(validateToonDocument), vscode.workspace.onDidChangeTextDocument((e) => { - onDocumentEventDebounced(e.document) + validateDebounced(e.document) }), vscode.workspace.onDidCloseTextDocument((document) => { + const key = document.uri.toString() + const timer = debounceTimers.get(key) + if (timer) { + clearTimeout(timer) + debounceTimers.delete(key) + } diagnosticCollection.delete(document.uri) }), ) - vscode.workspace.textDocuments.forEach(onDocumentEvent) + vscode.workspace.textDocuments.forEach(validateToonDocument) } -function onDocumentEvent(document: vscode.TextDocument): void { - validateToonDocument(document) -} +function validateDebounced(document: vscode.TextDocument): void { + const key = document.uri.toString() + const existing = debounceTimers.get(key) + if (existing) + clearTimeout(existing) -function onDocumentEventDebounced(document: vscode.TextDocument): void { - if (debounceTimer) - clearTimeout(debounceTimer) - debounceTimer = setTimeout(() => validateToonDocument(document), 300) + debounceTimers.set( + key, + setTimeout(() => { + debounceTimers.delete(key) + validateToonDocument(document) + }, 300), + ) } function validateToonDocument(document: vscode.TextDocument): void { - if (document.languageId !== 'toon') { + if (document.languageId !== 'toon') return - } const config = vscode.workspace.getConfiguration('toon') if (!config.get('validation.enable', true)) { diff --git a/src/utils.ts b/src/utils.ts index 783e705..e1b0ff9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,7 +14,6 @@ export function getDecodeOptions(): ToonToJsonOptions { const config = vscode.workspace.getConfiguration('toon') return { indent: config.get('json.indent', 2), - strict: config.get('decode.strict', true), expandPaths: config.get<'off' | 'safe'>('decode.expandPaths', 'off'), } } From 4a55e9d7d8eb488fff67be91eea531a572d2c218 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 11:48:06 +0800 Subject: [PATCH 12/16] docs: add README, CHANGELOG, and extension icon for v0.1.0 - Rewrite README with full feature documentation, usage guide, settings table - Add CHANGELOG.md for marketplace - Add extension icon (SVG) Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 17 ++++++++ README.md | 111 ++++++++++++++++++++++++++---------------------- images/icon.svg | 1 + 3 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 images/icon.svg diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5002af6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## 0.1.0 + +Initial release. + +### Features + +- Bidirectional JSON/JSONC to TOON conversion +- Selection-aware conversion in any file type +- Explorer context menu for file-to-file conversion with save dialog +- Editor context menu when text is selected +- Keyboard shortcuts: `Cmd+Alt+T` / `Ctrl+Alt+T` (to TOON), `Cmd+Alt+J` / `Ctrl+Alt+J` (to JSON) +- TextMate grammar for `.toon` syntax highlighting +- Real-time TOON validation with diagnostics in the Problems panel +- Configurable encoding options (indent, delimiter, key folding) +- Configurable decoding options (path expansion) diff --git a/README.md b/README.md index 1a3c884..d5795f7 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,88 @@ -# TOON Format for Visual Studio Code +# TOON — JSON ↔ TOON Converter for VS Code -> **⚠️ Development Status:** This extension is in early development. Bare minimum setup for team collaboration. +Bidirectional conversion between JSON and [TOON](https://toonformat.dev) (Token-Oriented Object Notation), with syntax highlighting and real-time validation. -Visual Studio Code extension for TOON format support. TOON is a compact, human-readable serialization format for LLM contexts with 30-60% token reduction vs JSON. +TOON is a compact, human-readable format that achieves ~40% fewer tokens than JSON while maintaining lossless, deterministic round-trips. Designed for LLM contexts where every token has a cost. ## Features -Currently in development. Planned features: +- **Bidirectional conversion** — Convert JSON/JSONC to TOON and back +- **Selection-aware** — Select text in any file to convert just that fragment +- **Context menu** — Right-click `.json` or `.toon` files in the explorer +- **Keyboard shortcuts** — `Cmd+Alt+T` (to TOON), `Cmd+Alt+J` (to JSON) +- **Syntax highlighting** — Full TextMate grammar for `.toon` files +- **Real-time validation** — Errors appear in the Problems panel as you type +- **Configurable** — Indent size, delimiter style, key folding, path expansion -- Syntax highlighting for `.toon` files -- Format validation and error detection -- Code formatting and auto-completion -- Integration with TOON specification +## Usage -## Installation +### Convert in the editor -This extension is not yet published to the Visual Studio Marketplace. To install locally: +1. Open a `.json` file +2. Press `Cmd+Alt+T` (Mac) or `Ctrl+Alt+T` (Windows/Linux) +3. The file content is replaced with TOON and the language mode switches -```bash -git clone https://github.com/toon-format/vscode.git -cd toon-vscode -pnpm install -pnpm build -``` +To convert back: open the `.toon` file and press `Cmd+Alt+J` / `Ctrl+Alt+J`. -## Development +### Convert a selection -```bash -# Setup -git clone https://github.com/toon-format/vscode.git -cd toon-vscode -pnpm install +Select any JSON or TOON text in any file (`.md`, `.txt`, etc.) and: +- Right-click → "Convert JSON to TOON" or "Convert TOON to JSON" +- Or use the keyboard shortcuts -# Build -pnpm build +### Save as a new file -# Development mode (watch) -pnpm dev +Right-click a `.json` file in the Explorer → "Convert to TOON (Save As...)" +Right-click a `.toon` file in the Explorer → "Convert to JSON (Save As...)" -# Run linting -pnpm lint +A save dialog appears so you choose the output location. -# Type check -pnpm test:types +### Command Palette -# Package extension -pnpm package -``` +All commands are available via `Cmd+Shift+P`: +- `Convert JSON to TOON` +- `Convert TOON to JSON` +- `Convert to TOON (Save As...)` +- `Convert to JSON (Save As...)` -## Project Status & Roadmap +## Settings -Following semantic versioning towards 1.0.0: +| Setting | Default | Description | +|---------|---------|-------------| +| `toon.encode.indent` | `2` | Indentation size for TOON output | +| `toon.encode.delimiter` | `,` | Delimiter for tabular arrays (`,`, `\|`, `\t`) | +| `toon.encode.keyFolding` | `off` | Dotted key folding (`off` or `safe`) | +| `toon.decode.expandPaths` | `off` | Expand dotted keys on decode (`off` or `safe`) | +| `toon.json.indent` | `2` | Indentation size for JSON output | +| `toon.openAfterConvert` | `true` | Open converted file after saving | +| `toon.validation.enable` | `true` | Enable real-time validation in Problems panel | -- **v0.0.x** - Initial project setup, bare minimum structure (current) -- **v0.1.x** - Basic syntax highlighting and file recognition -- **v0.2.x** - Format validation and error detection -- **v0.3.x** - Code formatting and auto-completion -- **v1.0.0** - First stable release with full TOON format support +## Development + +Requires Node.js 22+ for build tooling. -See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. +```bash +git clone https://github.com/akumarpalo/vscode.git +cd vscode +pnpm install +pnpm build +``` -## Documentation +Press F5 in VS Code to launch the Extension Development Host. -- [📜 TOON Specification](https://github.com/toon-format/spec) - Official specification -- [🐛 Issues](https://github.com/toon-format/vscode/issues) - Bug reports and features -- [🤝 Contributing](CONTRIBUTING.md) - Contribution guidelines +```bash +pnpm dev # Watch mode +pnpm lint # Lint +pnpm test:types # Type check +pnpm package # Build .vsix +``` -## Related Projects +## Links -- [toon](https://github.com/toon-format/toon) - TypeScript implementation -- [toon-python](https://github.com/toon-format/toon-python) - Python implementation -- [toon-rust](https://github.com/toon-format/toon-rust) - Rust implementation +- [TOON Specification](https://github.com/toon-format/spec) +- [TOON TypeScript Library](https://github.com/toon-format/toon) +- [Issues](https://github.com/akumarpalo/vscode/issues) ## License -MIT License – see [LICENSE](LICENSE) for details +MIT diff --git a/images/icon.svg b/images/icon.svg new file mode 100644 index 0000000..42a67a9 --- /dev/null +++ b/images/icon.svg @@ -0,0 +1 @@ + From 8a741811b2a87939b277a4d1d394994c32f5dfca Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 11:48:23 +0800 Subject: [PATCH 13/16] chore: fix lint issues and update package script for pnpm - Fix import ordering (perfectionist/sort-imports) - Fix export ordering (perfectionist/sort-exports) - Add parens to arrow function params (style/arrow-parens) - Use --no-dependencies flag in vsce package (pnpm compatibility) Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- src/commands/index.ts | 4 ++-- src/commands/jsonToToon.ts | 2 +- src/commands/toonToJson.ts | 2 +- src/converter.ts | 4 ++-- src/diagnostics.ts | 2 +- src/utils.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 844bd86..573a75d 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,7 @@ "lint:fix": "eslint . --fix", "test": "echo \"No tests yet\" && exit 0", "test:types": "tsc --noEmit", - "package": "pnpm build && vsce package", + "package": "pnpm build && vsce package --no-dependencies", "publish": "pnpm build && vsce publish", "vscode:prepublish": "pnpm build" }, diff --git a/src/commands/index.ts b/src/commands/index.ts index 021d3fa..00d5bbb 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,4 @@ +export { convertToJsonFileCommand } from './convertToJsonFile' +export { convertToToonFileCommand } from './convertToToonFile' export { jsonToToonCommand } from './jsonToToon' export { toonToJsonCommand } from './toonToJson' -export { convertToToonFileCommand } from './convertToToonFile' -export { convertToJsonFileCommand } from './convertToJsonFile' diff --git a/src/commands/jsonToToon.ts b/src/commands/jsonToToon.ts index b1155a8..dd1042d 100644 --- a/src/commands/jsonToToon.ts +++ b/src/commands/jsonToToon.ts @@ -48,7 +48,7 @@ export async function jsonToToonCommand(): Promise { return } - const applied = await editor.edit(editBuilder => { + const applied = await editor.edit((editBuilder) => { editBuilder.replace(range, result.output) }) diff --git a/src/commands/toonToJson.ts b/src/commands/toonToJson.ts index c072509..b0b2ac0 100644 --- a/src/commands/toonToJson.ts +++ b/src/commands/toonToJson.ts @@ -47,7 +47,7 @@ export async function toonToJsonCommand(): Promise { return } - const applied = await editor.edit(editBuilder => { + const applied = await editor.edit((editBuilder) => { editBuilder.replace(range, result.output) }) diff --git a/src/converter.ts b/src/converter.ts index 8eb1f4e..5b532d6 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -1,7 +1,7 @@ -import { decode, encode } from '@toon-format/toon' import type { DecodeOptions, EncodeOptions } from '@toon-format/toon' -import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser' import type { ParseError } from 'jsonc-parser' +import { decode, encode } from '@toon-format/toon' +import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser' export interface ConversionSuccess { success: true diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 7c64b69..da2b566 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,5 +1,5 @@ -import * as vscode from 'vscode' import { decode } from '@toon-format/toon' +import * as vscode from 'vscode' let diagnosticCollection: vscode.DiagnosticCollection const debounceTimers = new Map>() diff --git a/src/utils.ts b/src/utils.ts index e1b0ff9..525a18a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ -import * as vscode from 'vscode' import type { JsonToToonOptions, ToonToJsonOptions } from './converter' +import * as vscode from 'vscode' export function getEncodeOptions(): JsonToToonOptions { const config = vscode.workspace.getConfiguration('toon') From eca4fb470d8dec00ea80fca95a0dcae95f658767 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 14:10:14 +0800 Subject: [PATCH 14/16] fix: bundle @toon-format/toon and jsonc-parser into extension output tsdown was treating dependencies as external despite only 'vscode' being listed in the external config. Added explicit noExternal to force bundling. Without this, the extension fails to activate since the .vsix has no node_modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- tsdown.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tsdown.config.ts b/tsdown.config.ts index 24ff907..a4e804b 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'tsdown/config' export default defineConfig({ entry: 'src/extension.ts', external: ['vscode'], + noExternal: ['@toon-format/toon', 'jsonc-parser'], dts: false, clean: true, }) From 38ca5c452296d25ce55934c57e463723f0ff617e Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 14:20:55 +0800 Subject: [PATCH 15/16] fix: switch bundle output to CJS for reliable extension activation ESM output with createRequire shim was causing activation failures. CJS is the universally compatible format for VS Code extensions. Output file changed from dist/extension.mjs to dist/extension.cjs. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- tsdown.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 573a75d..5ab7d8b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "Programming Languages", "Formatters" ], - "main": "./dist/extension.mjs", + "main": "./dist/extension.cjs", "engines": { "vscode": "^1.95.0", "node": ">=18.0.0" diff --git a/tsdown.config.ts b/tsdown.config.ts index a4e804b..841906e 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'tsdown/config' export default defineConfig({ entry: 'src/extension.ts', + format: 'cjs', external: ['vscode'], noExternal: ['@toon-format/toon', 'jsonc-parser'], dts: false, From d2a7060cf2cbf34143ae63cad87231cd4d23dd25 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 4 Jun 2026 14:32:53 +0800 Subject: [PATCH 16/16] fix: resolve jsonc-parser bundling issue causing activation failure The UMD entry of jsonc-parser has internal require('./impl/format') calls that don't resolve when bundled into a single file. Fixed by aliasing jsonc-parser to its ESM entry point which gets fully inlined. Also removed "type": "module" from package.json and switched main to dist/extension.cjs for reliable CJS loading in VS Code. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 1 - tsdown.config.ts | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ab7d8b..8da8f9c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "publisher": "toon-format", "name": "toon", "displayName": "TOON — JSON ↔ TOON Converter", - "type": "module", "version": "0.1.0", "packageManager": "pnpm@10.23.0", "description": "Bidirectional JSON/TOON conversion, syntax highlighting, and real-time validation for the TOON format", diff --git a/tsdown.config.ts b/tsdown.config.ts index 841906e..dccc548 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,10 +1,15 @@ import { defineConfig } from 'tsdown/config' +import { resolve } from 'node:path' export default defineConfig({ entry: 'src/extension.ts', format: 'cjs', + outDir: 'dist', external: ['vscode'], noExternal: ['@toon-format/toon', 'jsonc-parser'], dts: false, clean: true, + alias: { + 'jsonc-parser': resolve('node_modules/jsonc-parser/lib/esm/main.js'), + }, })