From b8720e69081a85bc995f10ae97f397de67c12527 Mon Sep 17 00:00:00 2001 From: korone00 Date: Wed, 20 Aug 2025 12:40:09 +0900 Subject: [PATCH] fix: improve JSON path navigation using CodeMirror syntaxTree API --- package-lock.json | 7 +- package.json | 11 +- src/lib/components/JsonEditor.svelte | 230 ++++++++++++++++++++++++--- 3 files changed, 218 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 912fe3e..f6724c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.11.3", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", @@ -120,9 +121,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", diff --git a/package.json b/package.json index 5f1962b..2e19171 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,12 @@ "@typescript-eslint/parser": "^8.39.0", "autoprefixer": "^10.4.21", "bits-ui": "^2.9.1", + "cors": "^2.8.5", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-svelte": "^3.11.0", + "express": "^5.1.0", "globals": "^16.3.0", "paneforge": "^1.0.2", "postcss": "^8.5.6", @@ -51,12 +53,11 @@ "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.6", "typescript": "^5.0.0", - "vite": "^7.0.4", - "express": "^5.1.0", - "cors": "^2.8.5" + "vite": "^7.0.4" }, "dependencies": { "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.11.3", "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.1", @@ -69,7 +70,7 @@ "lucide-svelte": "^0.536.0", "mode-watcher": "^1.1.0", "tailwind-merge": "^3.3.1", - "typesafe-i18n": "^5.26.2", - "tslog": "^4.9.3" + "tslog": "^4.9.3", + "typesafe-i18n": "^5.26.2" } } diff --git a/src/lib/components/JsonEditor.svelte b/src/lib/components/JsonEditor.svelte index 9099a46..1099158 100644 --- a/src/lib/components/JsonEditor.svelte +++ b/src/lib/components/JsonEditor.svelte @@ -3,6 +3,7 @@ import { EditorView, basicSetup } from 'codemirror'; import { EditorState } from '@codemirror/state'; import { json } from '@codemirror/lang-json'; + import { syntaxTree } from '@codemirror/language'; import { oneDark } from '@codemirror/theme-one-dark'; import { mode } from 'mode-watcher'; import { logger } from '$lib/logger'; @@ -14,33 +15,218 @@ let { value = $bindable(''), class: className = '' }: Props = $props(); + // Build a map of JSON paths to their character positions using CodeMirror's syntax tree + function buildPathToPositionMap(state: EditorState): Map { + const pathMap = new Map(); + const tree = syntaxTree(state); + + // Helper to get text content of a node + function getNodeText(from: number, to: number): string { + return state.doc.sliceString(from, to); + } + + // Recursive function to traverse with path context + function traverse(cursor: any, path: string[] = []) { + do { + const nodeName = cursor.name; + + if (nodeName === 'Property') { + // Handle object properties + let propertyKey = ''; + let valueStart = -1; + let valueEnd = -1; + let valueType = ''; + + if (cursor.firstChild()) { + // Get property name + if (cursor.name === 'PropertyName') { + const keyText = getNodeText(cursor.from, cursor.to); + propertyKey = keyText.replace(/^"|"$/g, ''); + } + + // Skip to value (past the colon) + while (cursor.nextSibling()) { + if (cursor.name !== ':') { + valueStart = cursor.from; + valueEnd = cursor.to; + valueType = cursor.name; + break; + } + } + + // Store the path and position + if (propertyKey && valueStart !== -1) { + const valuePath = [...path, propertyKey]; + const pathStr = valuePath.join('.'); + + if (pathStr) { + pathMap.set(pathStr, { start: valueStart, end: valueEnd }); + } + + // If value is an array, handle array elements specially + if (valueType === 'Array') { + if (cursor.firstChild()) { + // We're now inside the array, process its elements + let index = 0; + do { + if (cursor.name === 'Object') { + const elementPath = [...valuePath, index.toString()]; + const elementPathStr = elementPath.join('.'); + + if (elementPathStr) { + pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); + } + + // Traverse into the object + if (cursor.firstChild()) { + traverse(cursor, elementPath); + cursor.parent(); + } + + index++; + } else if (cursor.name === 'Array') { + const elementPath = [...valuePath, index.toString()]; + const elementPathStr = elementPath.join('.'); + + if (elementPathStr) { + pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); + } + + // Recursive array + if (cursor.firstChild()) { + traverse(cursor, elementPath); + cursor.parent(); + } + + index++; + } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') { + // Primitive values in array + const elementPath = [...valuePath, index.toString()]; + const elementPathStr = elementPath.join('.'); + + if (elementPathStr) { + pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to }); + } + + index++; + } + } while (cursor.nextSibling()); + cursor.parent(); + } + } else if (valueType === 'Object') { + // If value is an object, traverse it normally + if (cursor.firstChild()) { + traverse(cursor, valuePath); + cursor.parent(); + } + } + } + + cursor.parent(); + } + } else if (nodeName === 'Array') { + // Handle array elements + let index = 0; + if (cursor.firstChild()) { + do { + // Only process actual elements (skip syntax tokens) + if (cursor.name === 'Object') { + // For object elements, store the indexed path and traverse + const elementPath = [...path, index.toString()]; + const pathStr = elementPath.join('.'); + + // Store the position of this array element + if (pathStr) { + pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); + } + + // Traverse into the object to get its properties + if (cursor.firstChild()) { + traverse(cursor, elementPath); + cursor.parent(); + } + + index++; + } else if (cursor.name === 'Array') { + // For nested arrays + const elementPath = [...path, index.toString()]; + const pathStr = elementPath.join('.'); + + if (pathStr) { + pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); + } + + // Traverse into the nested array + if (cursor.firstChild()) { + traverse(cursor, elementPath); + cursor.parent(); + } + + index++; + } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') { + // For primitive values + const elementPath = [...path, index.toString()]; + const pathStr = elementPath.join('.'); + + if (pathStr) { + pathMap.set(pathStr, { start: cursor.from, end: cursor.to }); + } + + index++; + } + } while (cursor.nextSibling()); + cursor.parent(); + } + } else if (nodeName === 'Object' && path.length > 0) { + // For nested objects in arrays, traverse their properties + if (cursor.firstChild()) { + traverse(cursor, path); + cursor.parent(); + } + } else if (nodeName === 'JsonText') { + // Root of JSON document + if (cursor.firstChild()) { + // Root can be object or array + if (cursor.name === 'Object' || cursor.name === 'Array') { + if (cursor.firstChild()) { + traverse(cursor, []); + cursor.parent(); + } + } + cursor.parent(); + } + } + } while (cursor.nextSibling()); + } + + // Start traversal + const cursor = tree.cursor(); + traverse(cursor); + + return pathMap; + } + export function navigateToPath(path: string) { if (!view) return; - const doc = view.state.doc; - const text = doc.toString(); - try { - JSON.parse(text); // Validate JSON - const pathParts = path.split('.'); - let line = 1; - - // Simple line counting - find the line containing the path - const lines = text.split('\n'); - for (let i = 0; i < lines.length; i++) { - if (pathParts.length > 0 && lines[i].includes(`"${pathParts[pathParts.length - 1]}"`)) { - line = i + 1; - break; - } + // Build the path to position map using syntax tree + const pathMap = buildPathToPositionMap(view.state); + + // Look up the position for this path + const position = pathMap.get(path); + + if (position) { + // Navigate to the found position + view.dispatch({ + selection: { anchor: position.start, head: position.end }, + scrollIntoView: true + }); + view.focus(); + logger.debug(`[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}`); + } else { + logger.warn(`[JsonEditor] Path not found: ${path}`); } - - // Scroll to line - const lineInfo = doc.line(line); - view.dispatch({ - selection: { anchor: lineInfo.from, head: lineInfo.to }, - scrollIntoView: true - }); - view.focus(); } catch (e) { logger.error('Failed to navigate to path:', e); }