Skip to content

Commit f457fc8

Browse files
committed
fix: improve JSON path navigation using CodeMirror syntaxTree API
1 parent 221d2e0 commit f457fc8

3 files changed

Lines changed: 218 additions & 30 deletions

File tree

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
"@typescript-eslint/parser": "^8.39.0",
3737
"autoprefixer": "^10.4.21",
3838
"bits-ui": "^2.9.1",
39+
"cors": "^2.8.5",
3940
"eslint": "^9.32.0",
4041
"eslint-config-prettier": "^10.1.8",
4142
"eslint-plugin-prettier": "^5.5.4",
4243
"eslint-plugin-svelte": "^3.11.0",
44+
"express": "^5.1.0",
4345
"globals": "^16.3.0",
4446
"paneforge": "^1.0.2",
4547
"postcss": "^8.5.6",
@@ -52,12 +54,11 @@
5254
"tailwindcss": "^4.1.11",
5355
"tw-animate-css": "^1.3.6",
5456
"typescript": "^5.0.0",
55-
"vite": "^7.0.4",
56-
"express": "^5.1.0",
57-
"cors": "^2.8.5"
57+
"vite": "^7.0.4"
5858
},
5959
"dependencies": {
6060
"@codemirror/lang-json": "^6.0.2",
61+
"@codemirror/language": "^6.11.3",
6162
"@codemirror/state": "^6.5.2",
6263
"@codemirror/theme-one-dark": "^6.1.3",
6364
"@codemirror/view": "^6.38.1",
@@ -70,7 +71,7 @@
7071
"lucide-svelte": "^0.536.0",
7172
"mode-watcher": "^1.1.0",
7273
"tailwind-merge": "^3.3.1",
73-
"typesafe-i18n": "^5.26.2",
74-
"tslog": "^4.9.3"
74+
"tslog": "^4.9.3",
75+
"typesafe-i18n": "^5.26.2"
7576
}
7677
}

src/lib/components/JsonEditor.svelte

Lines changed: 208 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { EditorView, basicSetup } from 'codemirror';
44
import { EditorState } from '@codemirror/state';
55
import { json } from '@codemirror/lang-json';
6+
import { syntaxTree } from '@codemirror/language';
67
import { oneDark } from '@codemirror/theme-one-dark';
78
import { mode } from 'mode-watcher';
89
import { logger } from '$lib/logger';
@@ -14,33 +15,218 @@
1415
1516
let { value = $bindable(''), class: className = '' }: Props = $props();
1617
18+
// Build a map of JSON paths to their character positions using CodeMirror's syntax tree
19+
function buildPathToPositionMap(state: EditorState): Map<string, { start: number; end: number }> {
20+
const pathMap = new Map<string, { start: number; end: number }>();
21+
const tree = syntaxTree(state);
22+
23+
// Helper to get text content of a node
24+
function getNodeText(from: number, to: number): string {
25+
return state.doc.sliceString(from, to);
26+
}
27+
28+
// Recursive function to traverse with path context
29+
function traverse(cursor: any, path: string[] = []) {
30+
do {
31+
const nodeName = cursor.name;
32+
33+
if (nodeName === 'Property') {
34+
// Handle object properties
35+
let propertyKey = '';
36+
let valueStart = -1;
37+
let valueEnd = -1;
38+
let valueType = '';
39+
40+
if (cursor.firstChild()) {
41+
// Get property name
42+
if (cursor.name === 'PropertyName') {
43+
const keyText = getNodeText(cursor.from, cursor.to);
44+
propertyKey = keyText.replace(/^"|"$/g, '');
45+
}
46+
47+
// Skip to value (past the colon)
48+
while (cursor.nextSibling()) {
49+
if (cursor.name !== ':') {
50+
valueStart = cursor.from;
51+
valueEnd = cursor.to;
52+
valueType = cursor.name;
53+
break;
54+
}
55+
}
56+
57+
// Store the path and position
58+
if (propertyKey && valueStart !== -1) {
59+
const valuePath = [...path, propertyKey];
60+
const pathStr = valuePath.join('.');
61+
62+
if (pathStr) {
63+
pathMap.set(pathStr, { start: valueStart, end: valueEnd });
64+
}
65+
66+
// If value is an array, handle array elements specially
67+
if (valueType === 'Array') {
68+
if (cursor.firstChild()) {
69+
// We're now inside the array, process its elements
70+
let index = 0;
71+
do {
72+
if (cursor.name === 'Object') {
73+
const elementPath = [...valuePath, index.toString()];
74+
const elementPathStr = elementPath.join('.');
75+
76+
if (elementPathStr) {
77+
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
78+
}
79+
80+
// Traverse into the object
81+
if (cursor.firstChild()) {
82+
traverse(cursor, elementPath);
83+
cursor.parent();
84+
}
85+
86+
index++;
87+
} else if (cursor.name === 'Array') {
88+
const elementPath = [...valuePath, index.toString()];
89+
const elementPathStr = elementPath.join('.');
90+
91+
if (elementPathStr) {
92+
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
93+
}
94+
95+
// Recursive array
96+
if (cursor.firstChild()) {
97+
traverse(cursor, elementPath);
98+
cursor.parent();
99+
}
100+
101+
index++;
102+
} else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '') {
103+
// Primitive values in array
104+
const elementPath = [...valuePath, index.toString()];
105+
const elementPathStr = elementPath.join('.');
106+
107+
if (elementPathStr) {
108+
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
109+
}
110+
111+
index++;
112+
}
113+
} while (cursor.nextSibling());
114+
cursor.parent();
115+
}
116+
} else if (valueType === 'Object') {
117+
// If value is an object, traverse it normally
118+
if (cursor.firstChild()) {
119+
traverse(cursor, valuePath);
120+
cursor.parent();
121+
}
122+
}
123+
}
124+
125+
cursor.parent();
126+
}
127+
} else if (nodeName === 'Array') {
128+
// Handle array elements
129+
let index = 0;
130+
if (cursor.firstChild()) {
131+
do {
132+
// Only process actual elements (skip syntax tokens)
133+
if (cursor.name === 'Object') {
134+
// For object elements, store the indexed path and traverse
135+
const elementPath = [...path, index.toString()];
136+
const pathStr = elementPath.join('.');
137+
138+
// Store the position of this array element
139+
if (pathStr) {
140+
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
141+
}
142+
143+
// Traverse into the object to get its properties
144+
if (cursor.firstChild()) {
145+
traverse(cursor, elementPath);
146+
cursor.parent();
147+
}
148+
149+
index++;
150+
} else if (cursor.name === 'Array') {
151+
// For nested arrays
152+
const elementPath = [...path, index.toString()];
153+
const pathStr = elementPath.join('.');
154+
155+
if (pathStr) {
156+
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
157+
}
158+
159+
// Traverse into the nested array
160+
if (cursor.firstChild()) {
161+
traverse(cursor, elementPath);
162+
cursor.parent();
163+
}
164+
165+
index++;
166+
} else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '') {
167+
// For primitive values
168+
const elementPath = [...path, index.toString()];
169+
const pathStr = elementPath.join('.');
170+
171+
if (pathStr) {
172+
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
173+
}
174+
175+
index++;
176+
}
177+
} while (cursor.nextSibling());
178+
cursor.parent();
179+
}
180+
} else if (nodeName === 'Object' && path.length > 0) {
181+
// For nested objects in arrays, traverse their properties
182+
if (cursor.firstChild()) {
183+
traverse(cursor, path);
184+
cursor.parent();
185+
}
186+
} else if (nodeName === 'JsonText') {
187+
// Root of JSON document
188+
if (cursor.firstChild()) {
189+
// Root can be object or array
190+
if (cursor.name === 'Object' || cursor.name === 'Array') {
191+
if (cursor.firstChild()) {
192+
traverse(cursor, []);
193+
cursor.parent();
194+
}
195+
}
196+
cursor.parent();
197+
}
198+
}
199+
} while (cursor.nextSibling());
200+
}
201+
202+
// Start traversal
203+
const cursor = tree.cursor();
204+
traverse(cursor);
205+
206+
return pathMap;
207+
}
208+
17209
export function navigateToPath(path: string) {
18210
if (!view) return;
19211
20-
const doc = view.state.doc;
21-
const text = doc.toString();
22-
23212
try {
24-
JSON.parse(text); // Validate JSON
25-
const pathParts = path.split('.');
26-
let line = 1;
27-
28-
// Simple line counting - find the line containing the path
29-
const lines = text.split('\n');
30-
for (let i = 0; i < lines.length; i++) {
31-
if (pathParts.length > 0 && lines[i].includes(`"${pathParts[pathParts.length - 1]}"`)) {
32-
line = i + 1;
33-
break;
34-
}
213+
// Build the path to position map using syntax tree
214+
const pathMap = buildPathToPositionMap(view.state);
215+
216+
// Look up the position for this path
217+
const position = pathMap.get(path);
218+
219+
if (position) {
220+
// Navigate to the found position
221+
view.dispatch({
222+
selection: { anchor: position.start, head: position.end },
223+
scrollIntoView: true
224+
});
225+
view.focus();
226+
logger.debug(`[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}`);
227+
} else {
228+
logger.warn(`[JsonEditor] Path not found: ${path}`);
35229
}
36-
37-
// Scroll to line
38-
const lineInfo = doc.line(line);
39-
view.dispatch({
40-
selection: { anchor: lineInfo.from, head: lineInfo.to },
41-
scrollIntoView: true
42-
});
43-
view.focus();
44230
} catch (e) {
45231
logger.error('Failed to navigate to path:', e);
46232
}

0 commit comments

Comments
 (0)