diff --git a/.vscodeignore b/.vscodeignore index ea3b34a..5d6a5e8 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -10,4 +10,6 @@ vsc-extension-quickstart.md **/*.ts **/.vscode-test.* **/.github -**/src/test \ No newline at end of file +**/src/test +**/*.env +**/.env* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 25ae13f..5c99888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jsondbg", - "version": "0.1.3", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jsondbg", - "version": "0.1.3", + "version": "0.2.0", "license": "MIT", "dependencies": { "jsoneditor": "^10.2.0" @@ -3421,9 +3421,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/src/jsonViewer.ts b/src/jsonViewer.ts index 5ed886a..332e828 100644 --- a/src/jsonViewer.ts +++ b/src/jsonViewer.ts @@ -1,9 +1,160 @@ // Generated by Copilot import * as vscode from 'vscode'; +/** + * Checks if a string is likely to be JSON before attempting to parse it + * @param str The string to check + * @returns An object containing whether the string is likely JSON and a reason if not + */ +function isLikelyJson(str: string): { isLikely: boolean, reason?: string } { + // Trim the string to remove whitespace + str = str.trim(); + + // Empty string is not JSON + if (!str) { + return { isLikely: false, reason: 'Empty string is not valid JSON' }; + } + + // Check if the string starts with either { or [ which are valid JSON starters + if (!(str.startsWith('{') || str.startsWith('['))) { + return { isLikely: false, reason: 'JSON must start with { or [' }; + } + + // Check if the string ends with matching closing bracket/brace + if ((str.startsWith('{') && !str.endsWith('}')) || + (str.startsWith('[') && !str.endsWith(']'))) { + return { isLikely: false, reason: 'Missing closing bracket/brace' }; + } + + // Basic check for balanced brackets/braces + const stack: string[] = []; + const openingBrackets = ['{', '[']; + const closingBrackets = ['}', ']']; + const pairs: {[key: string]: string} = {'}': '{', ']': '['}; + + let inString = false; + let escapeNext = false; + + for (let i = 0; i < str.length; i++) { + const char = str[i]; + + // Handle string literals and escaping + if (char === '"' && !escapeNext) { + inString = !inString; + } else if (char === '\\' && inString && !escapeNext) { + escapeNext = true; + continue; + } + + escapeNext = false; + + // Skip characters within string literals + if (inString) { + continue; + } + + // Check for opening brackets + if (openingBrackets.includes(char)) { + stack.push(char); + } + // Check for closing brackets + else if (closingBrackets.includes(char)) { + if (stack.length === 0 || stack.pop() !== pairs[char]) { + return { isLikely: false, reason: 'Unbalanced brackets/braces' }; + } + } + } + + // If we finish with unbalanced brackets, it's not valid JSON + if (stack.length > 0) { + return { isLikely: false, reason: 'Unbalanced brackets/braces' }; + } + + // Check for common JSON issues like trailing commas + if (str.includes(',]') || str.includes(',}')) { + return { isLikely: false, reason: 'Trailing commas are not allowed in JSON' }; + } + + return { isLikely: true }; +} + +/** + * Attempts to preprocess a string to make it JSON-parseable + * @param jsonValue The string to preprocess + * @returns The preprocessed string + */ +function preprocessJsonString(jsonValue: string): string { + // If the value is enclosed in quotes (string representation), remove them + if (jsonValue.startsWith('"') && jsonValue.endsWith('"')) { + jsonValue = jsonValue.substring(1, jsonValue.length - 1); + // Unescape any escaped quotes + jsonValue = jsonValue.replace(/\\"/g, '"'); + // Unescape other common escape sequences + jsonValue = jsonValue.replace(/\\([bfnrt])/g, (match, p1) => { + const escapeMap: {[key: string]: string} = { + 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t' + }; + return escapeMap[p1] || match; + }); + } + + // Clean up line endings - handle various escaped line endings + jsonValue = jsonValue.replace(/\\r\\n|\\n|\\r/g, ' ').trim(); + + // Fix common whitespace issues in debugging output + jsonValue = jsonValue.replace(/\\t/g, ' '); + + return jsonValue; +} + +/** + * Gets a more detailed error message for JSON parsing errors + * @param error The error from JSON.parse + * @param jsonValue The original string that failed to parse + * @param variableName The name of the variable for context + * @returns A detailed error message with suggestions + */ +function getDetailedJsonErrorMessage(error: Error, jsonValue: string, variableName: string): string { + let errorMessage = `"${variableName}" does not appear to be valid JSON`; + + // Extract position from SyntaxError message if available + const positionMatch = error.message.match(/position (\d+)/); + let position = -1; + + if (positionMatch && positionMatch[1]) { + position = parseInt(positionMatch[1], 10); + } + + // If we have a position, try to provide more context + if (position >= 0) { + // Get a snippet of the JSON around the error + const start = Math.max(0, position - 10); + const end = Math.min(jsonValue.length, position + 10); + const snippet = jsonValue.substring(start, end); + + errorMessage += `\n\nError near: ...${snippet}...`; + + // Add a pointer to the exact position + const pointerPadding = '...'.length + (position - start); + errorMessage += `\n${' '.repeat(pointerPadding)}^`; + } + + // Add the original error message + errorMessage += `\n\n${error.message}`; + + // Add tips for common JSON errors + if (error.message.includes('Unexpected token')) { + errorMessage += "\n\nCommon issues: Missing commas between elements, extra commas at end of lists/objects, or unquoted property names."; + } else if (error.message.includes('Unexpected end')) { + errorMessage += "\n\nCheck for missing closing brackets/braces or incomplete string literals."; + } + + return errorMessage; +} + // Register the viewJson command for debug variables export function registerViewJsonCommand(context: vscode.ExtensionContext): void { - const viewJsonCommand = vscode.commands.registerCommand('jsondbg.viewJson', async (variable) => { + const viewJsonCommand = vscode.commands.registerCommand('jsondbg.viewJson', async (variable: any) => { // Get the active debug session const session = vscode.debug.activeDebugSession; if (!session) { @@ -40,28 +191,30 @@ export function registerViewJsonCommand(context: vscode.ExtensionContext): void } else if (variable && variable.variable && variable.variable.name) { variableName = variable.variable.name; } + + // Preprocess the JSON string to improve parsing chances + const processedValue = preprocessJsonString(jsonValue); - // If the value is enclosed in quotes (string representation), remove them - if (jsonValue.startsWith('"') && jsonValue.endsWith('"')) { - jsonValue = jsonValue.substring(1, jsonValue.length - 1); - // Unescape any escaped quotes - jsonValue = jsonValue.replace(/\\"/g, '"'); + // Check if the value is likely JSON before attempting to parse + const likelyJson = isLikelyJson(processedValue); + if (!likelyJson.isLikely) { + vscode.window.showErrorMessage(`"${variableName}" does not appear to be valid JSON: ${likelyJson.reason}`); + return; } - // Clean up line endings - handle both \r\n and \n - jsonValue = jsonValue.replace(/\\r\\n|\\n|\\r/g, ' ').trim(); - try { // Try to parse the JSON string - const jsonObj = JSON.parse(jsonValue); + const jsonObj = JSON.parse(processedValue); // Create and show the JSON viewer createJsonViewer(jsonObj, context.extensionUri); } catch (error) { - let errorMessage = `"${variableName}" does not appear to be valid JSON`; + // Show a detailed error message if (error instanceof Error) { - errorMessage += `: ${error.message}`; + const detailedError = getDetailedJsonErrorMessage(error, processedValue, variableName); + vscode.window.showErrorMessage(detailedError, { modal: true }); + } else { + vscode.window.showErrorMessage(`"${variableName}" does not appear to be valid JSON`); } - vscode.window.showErrorMessage(errorMessage); } } catch (error) { let errorMessage = 'Error processing variable'; diff --git a/tsconfig.json b/tsconfig.json index 13f1cf3..d2d42d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "target": "ES2020", "outDir": "out", "lib": [ - "ES2020" + "ES2020", + "DOM" ], "sourceMap": true, "rootDir": "src",