Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ vsc-extension-quickstart.md
**/*.ts
**/.vscode-test.*
**/.github
**/src/test
**/src/test
**/*.env
**/.env*
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

179 changes: 166 additions & 13 deletions src/jsonViewer.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"target": "ES2020",
"outDir": "out",
"lib": [
"ES2020"
"ES2020",
"DOM"
],
"sourceMap": true,
"rootDir": "src",
Expand Down
Loading