From bc7098f64cd88d7368e189f076037014b3cbe18c Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Sun, 2 Aug 2026 22:26:02 +0530 Subject: [PATCH 1/4] feat: implement documentSymbol support for JSON objects and arrays --- language-server/src/build-server.ts | 2 + .../src/features/DocumentSymbols.test.ts | 198 ++++++++++++++++++ .../src/features/DocumentSymbols.ts | 132 ++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 language-server/src/features/DocumentSymbols.test.ts create mode 100644 language-server/src/features/DocumentSymbols.ts diff --git a/language-server/src/build-server.ts b/language-server/src/build-server.ts index b68dc1c..8af24ca 100644 --- a/language-server/src/build-server.ts +++ b/language-server/src/build-server.ts @@ -9,6 +9,7 @@ import { Formatting } from "./features/Formatting.ts"; import { Hover } from "./features/Hover.ts"; import { Completion } from "./features/Completion.ts"; import { FoldingRanges } from "./features/FoldingRanges.ts"; +import { DocumentSymbols } from "./features/DocumentSymbols.ts"; import "@hyperjump/json-schema/draft-2020-12"; import "@hyperjump/json-schema/draft-2019-09"; @@ -39,6 +40,7 @@ export const buildServer = (connection: Connection): Server => { new Hover(server, documents); new Completion(server, documents); new FoldingRanges(server, documents); + new DocumentSymbols(server, documents); return server; }; diff --git a/language-server/src/features/DocumentSymbols.test.ts b/language-server/src/features/DocumentSymbols.test.ts new file mode 100644 index 0000000..3809104 --- /dev/null +++ b/language-server/src/features/DocumentSymbols.test.ts @@ -0,0 +1,198 @@ +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { TestClient } from "../test/TestClient.ts"; +import { DocumentSymbolRequest, SymbolKind } from "vscode-languageserver"; + +describe("DocumentSymbols", () => { + let client: TestClient; + + beforeEach(async () => { + client = new TestClient(); + await client.start(); + }); + + afterEach(async () => { + await client.stop(); + }); + + test("should return document symbols for flat JSON object", async () => { + await client.writeDocument( + "test.json", + `{ + "name": "Alice", + "age": 30, + "active": true, + "address": null + }` + ); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(DocumentSymbolRequest.type, { + textDocument: { uri } + }); + + expect(result).toEqual([ + { + name: "name", + kind: SymbolKind.String, + range: { + start: { line: 1, character: 8 }, + end: { line: 1, character: 23 } + }, + selectionRange: { + start: { line: 1, character: 8 }, + end: { line: 1, character: 14 } + } + }, + { + name: "age", + kind: SymbolKind.Number, + range: { + start: { line: 2, character: 8 }, + end: { line: 2, character: 17 } + }, + selectionRange: { + start: { line: 2, character: 8 }, + end: { line: 2, character: 13 } + } + }, + { + name: "active", + kind: SymbolKind.Boolean, + range: { + start: { line: 3, character: 8 }, + end: { line: 3, character: 22 } + }, + selectionRange: { + start: { line: 3, character: 8 }, + end: { line: 3, character: 16 } + } + }, + { + name: "address", + kind: SymbolKind.Null, + range: { + start: { line: 4, character: 8 }, + end: { line: 4, character: 23 } + }, + selectionRange: { + start: { line: 4, character: 8 }, + end: { line: 4, character: 17 } + } + } + ]); + }); + + test("should return document symbols for nested JSON objects", async () => { + await client.writeDocument( + "test.json", + `{ + "server": { + "port": 8080 + } + }` + ); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(DocumentSymbolRequest.type, { + textDocument: { uri } + }); + + expect(result).toEqual([ + { + name: "server", + kind: SymbolKind.Object, + range: { + start: { line: 1, character: 8 }, + end: { line: 3, character: 9 } + }, + selectionRange: { + start: { line: 1, character: 8 }, + end: { line: 1, character: 16 } + }, + children: [ + { + name: "port", + kind: SymbolKind.Number, + range: { + start: { line: 2, character: 10 }, + end: { line: 2, character: 22 } + }, + selectionRange: { + start: { line: 2, character: 10 }, + end: { line: 2, character: 16 } + } + } + ] + } + ]); + }); + + test("should return document symbols for JSON arrays", async () => { + await client.writeDocument( + "test.json", + `{ + "plugins": [ + "auth", + "logger" + ] + }` + ); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(DocumentSymbolRequest.type, { + textDocument: { uri } + }); + + expect(result).toEqual([ + { + name: "plugins", + kind: SymbolKind.Array, + range: { + start: { line: 1, character: 8 }, + end: { line: 4, character: 9 } + }, + selectionRange: { + start: { line: 1, character: 8 }, + end: { line: 1, character: 17 } + }, + children: [ + { + name: "0", + kind: SymbolKind.String, + range: { + start: { line: 2, character: 10 }, + end: { line: 2, character: 16 } + }, + selectionRange: { + start: { line: 2, character: 10 }, + end: { line: 2, character: 16 } + } + }, + { + name: "1", + kind: SymbolKind.String, + range: { + start: { line: 3, character: 10 }, + end: { line: 3, character: 18 } + }, + selectionRange: { + start: { line: 3, character: 10 }, + end: { line: 3, character: 18 } + } + } + ] + } + ]); + }); + + test("should return empty array for empty JSON object or empty array root", async () => { + await client.writeDocument("test.json", "{}\n"); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(DocumentSymbolRequest.type, { + textDocument: { uri } + }); + + expect(result).toEqual([]); + }); +}); diff --git a/language-server/src/features/DocumentSymbols.ts b/language-server/src/features/DocumentSymbols.ts new file mode 100644 index 0000000..b44b824 --- /dev/null +++ b/language-server/src/features/DocumentSymbols.ts @@ -0,0 +1,132 @@ +import { SymbolKind } from "vscode-languageserver"; + +import type { DocumentSymbol, ServerCapabilities } from "vscode-languageserver"; +import type { Node } from "jsonc-parser"; +import type { Server } from "../services/Server.ts"; +import type { JsonDocuments } from "../services/JsonDocuments.ts"; +import type { JsonDocument } from "../models/JsonDocument.ts"; + +export class DocumentSymbols { + private jsonDocuments: JsonDocuments; + + constructor(server: Server, jsonDocuments: JsonDocuments) { + this.jsonDocuments = jsonDocuments; + + server.onInitialize(() => { + const serverCapabilities: ServerCapabilities = { + documentSymbolProvider: true + }; + + return { + capabilities: serverCapabilities + }; + }); + + server.onDocumentSymbol((params) => { + const jsonDocument = this.jsonDocuments.get(params.textDocument.uri); + if (!jsonDocument) { + return []; + } + + return this.getDocumentSymbols(jsonDocument); + }); + } + + private getDocumentSymbols(jsonDocument: JsonDocument): DocumentSymbol[] { + const ast = jsonDocument.findNodeAtPointer(""); + if (!ast) { + return []; + } + + return this.collectDocumentSymbols(jsonDocument, ast); + } + + private collectDocumentSymbols(jsonDocument: JsonDocument, node: Node): DocumentSymbol[] { + const symbols: DocumentSymbol[] = []; + + if (node.type === "object" && node.children) { + for (const child of node.children) { + if (child.type === "property" && child.children) { + const keyNode = child.children[0]; + const valueNode = child.children[1]; + + if (!keyNode) { + continue; + } + + const name = String(keyNode.value); + const range = { + start: jsonDocument.positionAt(child.offset), + end: jsonDocument.positionAt(child.offset + child.length) + }; + const selectionRange = { + start: jsonDocument.positionAt(keyNode.offset), + end: jsonDocument.positionAt(keyNode.offset + keyNode.length) + }; + + const kind = this.getSymbolKind(valueNode?.type); + const children = valueNode ? this.collectDocumentSymbols(jsonDocument, valueNode) : []; + + const symbol: DocumentSymbol = { + name, + kind, + range, + selectionRange + }; + + if (children.length > 0) { + symbol.children = children; + } + + symbols.push(symbol); + } + } + } else if (node.type === "array" && node.children) { + node.children.forEach((child, index) => { + const name = String(index); + const range = { + start: jsonDocument.positionAt(child.offset), + end: jsonDocument.positionAt(child.offset + child.length) + }; + const selectionRange = range; + + const kind = this.getSymbolKind(child.type); + const children = this.collectDocumentSymbols(jsonDocument, child); + + const symbol: DocumentSymbol = { + name, + kind, + range, + selectionRange + }; + + if (children.length > 0) { + symbol.children = children; + } + + symbols.push(symbol); + }); + } + + return symbols; + } + + private getSymbolKind(type?: string): SymbolKind { + switch (type) { + case "object": + return SymbolKind.Object; + case "array": + return SymbolKind.Array; + case "string": + return SymbolKind.String; + case "number": + return SymbolKind.Number; + case "boolean": + return SymbolKind.Boolean; + case "null": + return SymbolKind.Null; + default: + return SymbolKind.Property; + } + } +} From 20becd18ca1ead291ee43f436e1b4ba80724ec04 Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Tue, 4 Aug 2026 19:11:20 +0530 Subject: [PATCH 2/4] refactor: address review comments on DocumentSymbols handler and AST traversal --- .../src/features/DocumentSymbols.ts | 86 ++++++++----------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/language-server/src/features/DocumentSymbols.ts b/language-server/src/features/DocumentSymbols.ts index b44b824..5a3bbd1 100644 --- a/language-server/src/features/DocumentSymbols.ts +++ b/language-server/src/features/DocumentSymbols.ts @@ -23,66 +23,52 @@ export class DocumentSymbols { }); server.onDocumentSymbol((params) => { - const jsonDocument = this.jsonDocuments.get(params.textDocument.uri); - if (!jsonDocument) { + const jsonDocument = this.jsonDocuments.get(params.textDocument.uri)!; + const ast = jsonDocument.findNodeAtPointer(""); + if (!ast) { return []; } - return this.getDocumentSymbols(jsonDocument); + return this.collectDocumentSymbols(jsonDocument, ast); }); } - private getDocumentSymbols(jsonDocument: JsonDocument): DocumentSymbol[] { - const ast = jsonDocument.findNodeAtPointer(""); - if (!ast) { - return []; - } - - return this.collectDocumentSymbols(jsonDocument, ast); - } - private collectDocumentSymbols(jsonDocument: JsonDocument, node: Node): DocumentSymbol[] { const symbols: DocumentSymbol[] = []; - if (node.type === "object" && node.children) { - for (const child of node.children) { - if (child.type === "property" && child.children) { - const keyNode = child.children[0]; - const valueNode = child.children[1]; - - if (!keyNode) { - continue; - } - - const name = String(keyNode.value); - const range = { - start: jsonDocument.positionAt(child.offset), - end: jsonDocument.positionAt(child.offset + child.length) - }; - const selectionRange = { - start: jsonDocument.positionAt(keyNode.offset), - end: jsonDocument.positionAt(keyNode.offset + keyNode.length) - }; - - const kind = this.getSymbolKind(valueNode?.type); - const children = valueNode ? this.collectDocumentSymbols(jsonDocument, valueNode) : []; - - const symbol: DocumentSymbol = { - name, - kind, - range, - selectionRange - }; - - if (children.length > 0) { - symbol.children = children; - } - - symbols.push(symbol); + if (node.type === "object") { + for (const propertyNode of node.children!) { + const keyNode = propertyNode.children![0]; + const valueNode = propertyNode.children![1]; + + const name = String(keyNode.value); + const range = { + start: jsonDocument.positionAt(propertyNode.offset), + end: jsonDocument.positionAt(propertyNode.offset + propertyNode.length) + }; + const selectionRange = { + start: jsonDocument.positionAt(keyNode.offset), + end: jsonDocument.positionAt(keyNode.offset + keyNode.length) + }; + + const kind = this.getSymbolKind(valueNode?.type); + const children = valueNode ? this.collectDocumentSymbols(jsonDocument, valueNode) : []; + + const symbol: DocumentSymbol = { + name, + kind, + range, + selectionRange + }; + + if (children.length > 0) { + symbol.children = children; } + + symbols.push(symbol); } - } else if (node.type === "array" && node.children) { - node.children.forEach((child, index) => { + } else if (node.type === "array") { + node.children!.forEach((child, index) => { const name = String(index); const range = { start: jsonDocument.positionAt(child.offset), @@ -125,6 +111,8 @@ export class DocumentSymbols { return SymbolKind.Boolean; case "null": return SymbolKind.Null; + case "property": + return SymbolKind.Property; default: return SymbolKind.Property; } From 9782252d98edf963fc601f4843654843370b2413 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 4 Aug 2026 14:01:22 -0700 Subject: [PATCH 3/4] Cleanup --- .../src/features/DocumentSymbols.test.ts | 115 ++++++++---------- .../src/features/DocumentSymbols.ts | 17 +-- 2 files changed, 57 insertions(+), 75 deletions(-) diff --git a/language-server/src/features/DocumentSymbols.test.ts b/language-server/src/features/DocumentSymbols.test.ts index 3809104..d4ffd1a 100644 --- a/language-server/src/features/DocumentSymbols.test.ts +++ b/language-server/src/features/DocumentSymbols.test.ts @@ -15,15 +15,12 @@ describe("DocumentSymbols", () => { }); test("should return document symbols for flat JSON object", async () => { - await client.writeDocument( - "test.json", - `{ - "name": "Alice", - "age": 30, - "active": true, - "address": null - }` - ); + await client.writeDocument("test.json", `{ + "name": "Alice", + "age": 30, + "active": true, + "address": null + }`); const uri = await client.openDocument("test.json"); const result = await client.sendRequest(DocumentSymbolRequest.type, { @@ -35,62 +32,59 @@ describe("DocumentSymbols", () => { name: "name", kind: SymbolKind.String, range: { - start: { line: 1, character: 8 }, - end: { line: 1, character: 23 } + start: { line: 1, character: 6 }, + end: { line: 1, character: 21 } }, selectionRange: { - start: { line: 1, character: 8 }, - end: { line: 1, character: 14 } + start: { line: 1, character: 6 }, + end: { line: 1, character: 12 } } }, { name: "age", kind: SymbolKind.Number, range: { - start: { line: 2, character: 8 }, - end: { line: 2, character: 17 } + start: { line: 2, character: 6 }, + end: { line: 2, character: 15 } }, selectionRange: { - start: { line: 2, character: 8 }, - end: { line: 2, character: 13 } + start: { line: 2, character: 6 }, + end: { line: 2, character: 11 } } }, { name: "active", kind: SymbolKind.Boolean, range: { - start: { line: 3, character: 8 }, - end: { line: 3, character: 22 } + start: { line: 3, character: 6 }, + end: { line: 3, character: 20 } }, selectionRange: { - start: { line: 3, character: 8 }, - end: { line: 3, character: 16 } + start: { line: 3, character: 6 }, + end: { line: 3, character: 14 } } }, { name: "address", kind: SymbolKind.Null, range: { - start: { line: 4, character: 8 }, - end: { line: 4, character: 23 } + start: { line: 4, character: 6 }, + end: { line: 4, character: 21 } }, selectionRange: { - start: { line: 4, character: 8 }, - end: { line: 4, character: 17 } + start: { line: 4, character: 6 }, + end: { line: 4, character: 15 } } } ]); }); test("should return document symbols for nested JSON objects", async () => { - await client.writeDocument( - "test.json", - `{ - "server": { - "port": 8080 - } - }` - ); + await client.writeDocument("test.json", `{ + "server": { + "port": 8080 + } + }`); const uri = await client.openDocument("test.json"); const result = await client.sendRequest(DocumentSymbolRequest.type, { @@ -102,24 +96,24 @@ describe("DocumentSymbols", () => { name: "server", kind: SymbolKind.Object, range: { - start: { line: 1, character: 8 }, - end: { line: 3, character: 9 } + start: { line: 1, character: 6 }, + end: { line: 3, character: 7 } }, selectionRange: { - start: { line: 1, character: 8 }, - end: { line: 1, character: 16 } + start: { line: 1, character: 6 }, + end: { line: 1, character: 14 } }, children: [ { name: "port", kind: SymbolKind.Number, range: { - start: { line: 2, character: 10 }, - end: { line: 2, character: 22 } + start: { line: 2, character: 8 }, + end: { line: 2, character: 20 } }, selectionRange: { - start: { line: 2, character: 10 }, - end: { line: 2, character: 16 } + start: { line: 2, character: 8 }, + end: { line: 2, character: 14 } } } ] @@ -128,15 +122,12 @@ describe("DocumentSymbols", () => { }); test("should return document symbols for JSON arrays", async () => { - await client.writeDocument( - "test.json", - `{ - "plugins": [ - "auth", - "logger" - ] - }` - ); + await client.writeDocument("test.json", `{ + "plugins": [ + "auth", + "logger" + ] + }`); const uri = await client.openDocument("test.json"); const result = await client.sendRequest(DocumentSymbolRequest.type, { @@ -148,36 +139,36 @@ describe("DocumentSymbols", () => { name: "plugins", kind: SymbolKind.Array, range: { - start: { line: 1, character: 8 }, - end: { line: 4, character: 9 } + start: { line: 1, character: 6 }, + end: { line: 4, character: 7 } }, selectionRange: { - start: { line: 1, character: 8 }, - end: { line: 1, character: 17 } + start: { line: 1, character: 6 }, + end: { line: 1, character: 15 } }, children: [ { name: "0", kind: SymbolKind.String, range: { - start: { line: 2, character: 10 }, - end: { line: 2, character: 16 } + start: { line: 2, character: 8 }, + end: { line: 2, character: 14 } }, selectionRange: { - start: { line: 2, character: 10 }, - end: { line: 2, character: 16 } + start: { line: 2, character: 8 }, + end: { line: 2, character: 14 } } }, { name: "1", kind: SymbolKind.String, range: { - start: { line: 3, character: 10 }, - end: { line: 3, character: 18 } + start: { line: 3, character: 8 }, + end: { line: 3, character: 16 } }, selectionRange: { - start: { line: 3, character: 10 }, - end: { line: 3, character: 18 } + start: { line: 3, character: 8 }, + end: { line: 3, character: 16 } } } ] diff --git a/language-server/src/features/DocumentSymbols.ts b/language-server/src/features/DocumentSymbols.ts index 5a3bbd1..052b2ba 100644 --- a/language-server/src/features/DocumentSymbols.ts +++ b/language-server/src/features/DocumentSymbols.ts @@ -54,12 +54,7 @@ export class DocumentSymbols { const kind = this.getSymbolKind(valueNode?.type); const children = valueNode ? this.collectDocumentSymbols(jsonDocument, valueNode) : []; - const symbol: DocumentSymbol = { - name, - kind, - range, - selectionRange - }; + const symbol: DocumentSymbol = { name, kind, range, selectionRange }; if (children.length > 0) { symbol.children = children; @@ -79,12 +74,7 @@ export class DocumentSymbols { const kind = this.getSymbolKind(child.type); const children = this.collectDocumentSymbols(jsonDocument, child); - const symbol: DocumentSymbol = { - name, - kind, - range, - selectionRange - }; + const symbol: DocumentSymbol = { name, kind, range, selectionRange }; if (children.length > 0) { symbol.children = children; @@ -114,7 +104,8 @@ export class DocumentSymbols { case "property": return SymbolKind.Property; default: - return SymbolKind.Property; + // Unreachable code, but typescript needs something to know the function doesn't return undefined + throw Error("Unreachable"); } } } From baa0afb93d3aa8376185356663dccd1196b17c22 Mon Sep 17 00:00:00 2001 From: Suyog Habbu Date: Wed, 5 Aug 2026 17:39:20 +0530 Subject: [PATCH 4/4] generate two entries per property (Key & Value) --- .../src/features/DocumentSymbols.test.ts | 112 +++++++++++++++--- .../src/features/DocumentSymbols.ts | 46 ++++--- 2 files changed, 128 insertions(+), 30 deletions(-) diff --git a/language-server/src/features/DocumentSymbols.test.ts b/language-server/src/features/DocumentSymbols.test.ts index d4ffd1a..148c80c 100644 --- a/language-server/src/features/DocumentSymbols.test.ts +++ b/language-server/src/features/DocumentSymbols.test.ts @@ -30,51 +30,99 @@ describe("DocumentSymbols", () => { expect(result).toEqual([ { name: "name", - kind: SymbolKind.String, + kind: SymbolKind.Property, range: { start: { line: 1, character: 6 }, - end: { line: 1, character: 21 } + end: { line: 1, character: 12 } }, selectionRange: { start: { line: 1, character: 6 }, end: { line: 1, character: 12 } } }, + { + name: "Alice", + kind: SymbolKind.String, + range: { + start: { line: 1, character: 14 }, + end: { line: 1, character: 21 } + }, + selectionRange: { + start: { line: 1, character: 14 }, + end: { line: 1, character: 21 } + } + }, { name: "age", - kind: SymbolKind.Number, + kind: SymbolKind.Property, range: { start: { line: 2, character: 6 }, - end: { line: 2, character: 15 } + end: { line: 2, character: 11 } }, selectionRange: { start: { line: 2, character: 6 }, end: { line: 2, character: 11 } } }, + { + name: "30", + kind: SymbolKind.Number, + range: { + start: { line: 2, character: 13 }, + end: { line: 2, character: 15 } + }, + selectionRange: { + start: { line: 2, character: 13 }, + end: { line: 2, character: 15 } + } + }, { name: "active", - kind: SymbolKind.Boolean, + kind: SymbolKind.Property, range: { start: { line: 3, character: 6 }, - end: { line: 3, character: 20 } + end: { line: 3, character: 14 } }, selectionRange: { start: { line: 3, character: 6 }, end: { line: 3, character: 14 } } }, + { + name: "true", + kind: SymbolKind.Boolean, + range: { + start: { line: 3, character: 16 }, + end: { line: 3, character: 20 } + }, + selectionRange: { + start: { line: 3, character: 16 }, + end: { line: 3, character: 20 } + } + }, { name: "address", - kind: SymbolKind.Null, + kind: SymbolKind.Property, range: { start: { line: 4, character: 6 }, - end: { line: 4, character: 21 } + end: { line: 4, character: 15 } }, selectionRange: { start: { line: 4, character: 6 }, end: { line: 4, character: 15 } } + }, + { + name: "null", + kind: SymbolKind.Null, + range: { + start: { line: 4, character: 17 }, + end: { line: 4, character: 21 } + }, + selectionRange: { + start: { line: 4, character: 17 }, + end: { line: 4, character: 21 } + } } ]); }); @@ -94,27 +142,51 @@ describe("DocumentSymbols", () => { expect(result).toEqual([ { name: "server", - kind: SymbolKind.Object, + kind: SymbolKind.Property, range: { start: { line: 1, character: 6 }, - end: { line: 3, character: 7 } + end: { line: 1, character: 14 } }, selectionRange: { start: { line: 1, character: 6 }, end: { line: 1, character: 14 } + } + }, + { + name: "server", + kind: SymbolKind.Object, + range: { + start: { line: 1, character: 16 }, + end: { line: 3, character: 7 } + }, + selectionRange: { + start: { line: 1, character: 16 }, + end: { line: 3, character: 7 } }, children: [ { name: "port", - kind: SymbolKind.Number, + kind: SymbolKind.Property, range: { start: { line: 2, character: 8 }, - end: { line: 2, character: 20 } + end: { line: 2, character: 14 } }, selectionRange: { start: { line: 2, character: 8 }, end: { line: 2, character: 14 } } + }, + { + name: "8080", + kind: SymbolKind.Number, + range: { + start: { line: 2, character: 16 }, + end: { line: 2, character: 20 } + }, + selectionRange: { + start: { line: 2, character: 16 }, + end: { line: 2, character: 20 } + } } ] } @@ -137,14 +209,26 @@ describe("DocumentSymbols", () => { expect(result).toEqual([ { name: "plugins", - kind: SymbolKind.Array, + kind: SymbolKind.Property, range: { start: { line: 1, character: 6 }, - end: { line: 4, character: 7 } + end: { line: 1, character: 15 } }, selectionRange: { start: { line: 1, character: 6 }, end: { line: 1, character: 15 } + } + }, + { + name: "plugins", + kind: SymbolKind.Array, + range: { + start: { line: 1, character: 17 }, + end: { line: 4, character: 7 } + }, + selectionRange: { + start: { line: 1, character: 17 }, + end: { line: 4, character: 7 } }, children: [ { diff --git a/language-server/src/features/DocumentSymbols.ts b/language-server/src/features/DocumentSymbols.ts index 052b2ba..221f754 100644 --- a/language-server/src/features/DocumentSymbols.ts +++ b/language-server/src/features/DocumentSymbols.ts @@ -41,26 +41,40 @@ export class DocumentSymbols { const keyNode = propertyNode.children![0]; const valueNode = propertyNode.children![1]; - const name = String(keyNode.value); - const range = { - start: jsonDocument.positionAt(propertyNode.offset), - end: jsonDocument.positionAt(propertyNode.offset + propertyNode.length) - }; - const selectionRange = { + const keyRange = { start: jsonDocument.positionAt(keyNode.offset), end: jsonDocument.positionAt(keyNode.offset + keyNode.length) }; - - const kind = this.getSymbolKind(valueNode?.type); - const children = valueNode ? this.collectDocumentSymbols(jsonDocument, valueNode) : []; - - const symbol: DocumentSymbol = { name, kind, range, selectionRange }; - - if (children.length > 0) { - symbol.children = children; + const keySymbol: DocumentSymbol = { + name: String(keyNode.value), + kind: SymbolKind.Property, + range: keyRange, + selectionRange: keyRange + }; + symbols.push(keySymbol); + + if (valueNode) { + const valueRange = { + start: jsonDocument.positionAt(valueNode.offset), + end: jsonDocument.positionAt(valueNode.offset + valueNode.length) + }; + const valueName = valueNode.value !== undefined ? String(valueNode.value) : String(keyNode.value); + const kind = this.getSymbolKind(valueNode.type); + const children = this.collectDocumentSymbols(jsonDocument, valueNode); + + const valueSymbol: DocumentSymbol = { + name: valueName, + kind, + range: valueRange, + selectionRange: valueRange + }; + + if (children.length > 0) { + valueSymbol.children = children; + } + + symbols.push(valueSymbol); } - - symbols.push(symbol); } } else if (node.type === "array") { node.children!.forEach((child, index) => {