From 89dc593d7591013c2116f55f53bf6c509f9c8962 Mon Sep 17 00:00:00 2001 From: Diya Date: Sun, 9 Aug 2026 10:38:05 +0530 Subject: [PATCH 1/5] add selectionRange support --- language-server/src/build-server.ts | 2 + .../src/features/SelectionRanges.test.ts | 393 ++++++++++++++++++ .../src/features/SelectionRanges.ts | 89 ++++ 3 files changed, 484 insertions(+) create mode 100644 language-server/src/features/SelectionRanges.test.ts create mode 100644 language-server/src/features/SelectionRanges.ts diff --git a/language-server/src/build-server.ts b/language-server/src/build-server.ts index 8af24ca..1f09bc5 100644 --- a/language-server/src/build-server.ts +++ b/language-server/src/build-server.ts @@ -10,6 +10,7 @@ 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 { SelectionRanges } from "./features/SelectionRanges.ts"; import "@hyperjump/json-schema/draft-2020-12"; import "@hyperjump/json-schema/draft-2019-09"; @@ -41,6 +42,7 @@ export const buildServer = (connection: Connection): Server => { new Completion(server, documents); new FoldingRanges(server, documents); new DocumentSymbols(server, documents); + new SelectionRanges(server, documents); return server; }; diff --git a/language-server/src/features/SelectionRanges.test.ts b/language-server/src/features/SelectionRanges.test.ts new file mode 100644 index 0000000..793b29e --- /dev/null +++ b/language-server/src/features/SelectionRanges.test.ts @@ -0,0 +1,393 @@ +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { TestClient } from "../test/TestClient.ts"; +import { SelectionRangeRequest } from "vscode-languageserver"; + +describe("SelectionRanges", () => { + let client: TestClient; + + beforeEach(async () => { + client = new TestClient(); + await client.start(); + }); + + afterEach(async () => { + await client.stop(); + }); + + test("should return a nested chain of selection ranges for a string property value", async () => { + await client.writeDocument("test.json", `{ + "foo": "bar", + "baz": 123 + }`); + + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 1, character: 15 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 1, character: 14 }, + end: { line: 1, character: 17 } + }, + parent: { + range: { + start: { line: 1, character: 13 }, + end: { line: 1, character: 18 } + }, + parent: { + range: { + start: { line: 1, character: 6 }, + end: { line: 1, character: 18 } + }, + parent: { + range: { + start: { line: 1, character: 6 }, + end: { line: 1, character: 19 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 3, character: 4 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 3, character: 5 } + } + } + } + } + } + } + } + ]); + }); + + test("should include the trailing comma when the selected element is followed by another array element", async () => { + await client.writeDocument("test.json", `[1, 2, 3]`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 4 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 4 }, + end: { line: 0, character: 5 } + }, + parent: { + range: { + start: { line: 0, character: 4 }, + end: { line: 0, character: 6 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 9 } + } + } + } + } + } + ]); + }); + + test("should not include a trailing comma when the selected element is the last element in an array", async () => { + await client.writeDocument("test.json", `[1, 2, 3]`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 7 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 7 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 9 } + } + } + } + } + ]); + }); + + test("should handle multiple positions in a single request, one with a following property and one without", async () => { + await client.writeDocument("test.json", `{ + "foo": 1, + "bar": 2 + }`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [ + { line: 1, character: 13 }, + { line: 2, character: 13 } + ] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 1, character: 13 }, + end: { line: 1, character: 14 } + }, + parent: { + range: { + start: { line: 1, character: 6 }, + end: { line: 1, character: 14 } + }, + parent: { + range: { + start: { line: 1, character: 6 }, + end: { line: 1, character: 15 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 3, character: 4 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 3, character: 5 } + } + } + } + } + } + }, + { + range: { + start: { line: 2, character: 13 }, + end: { line: 2, character: 14 } + }, + parent: { + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 14 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 3, character: 4 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 3, character: 5 } + } + } + } + } + } + ]); + }); + + test("should thread the parent chain through mixed nested objects and arrays", async () => { + await client.writeDocument("test.json", `{ + "foo": { + "bar": [1, 2, {"baz": true}] + } + }`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 2, character: 31 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 2, character: 30 }, + end: { line: 2, character: 34 } + }, + parent: { + range: { + start: { line: 2, character: 23 }, + end: { line: 2, character: 34 } + }, + parent: { + range: { + start: { line: 2, character: 23 }, + end: { line: 2, character: 34 } + }, + parent: { + range: { + start: { line: 2, character: 22 }, + end: { line: 2, character: 35 } + }, + parent: { + range: { + start: { line: 2, character: 16 }, + end: { line: 2, character: 35 } + }, + parent: { + range: { + start: { line: 2, character: 15 }, + end: { line: 2, character: 36 } + }, + parent: { + range: { + start: { line: 2, character: 8 }, + end: { line: 2, character: 36 } + }, + parent: { + range: { + start: { line: 1, character: 14 }, + end: { line: 3, character: 6 } + }, + parent: { + range: { + start: { line: 1, character: 13 }, + end: { line: 3, character: 7 } + }, + parent: { + range: { + start: { line: 1, character: 6 }, + end: { line: 3, character: 7 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 4, character: 4 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 4, character: 5 } + } + } + } + } + } + } + } + } + } + } + } + } + } + ]); + }); + + test("should return the whole document as a single flat range for a position at the very start of the document", async () => { + await client.writeDocument("test.json", `{"a": 1}`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 0 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 8 } + } + } + ]); + }); + + test("should return a collapsed range with no parent for a position at the very end of the document", async () => { + await client.writeDocument("test.json", `{"a": 1}`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 8 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 8 }, + end: { line: 0, character: 8 } + } + } + ]); + }); + + test("should not return an inner range for an empty array", async () => { + await client.writeDocument("test.json", `{"a": []}`); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 6 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 6 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 1 }, + end: { line: 0, character: 8 } + }, + parent: { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 9 } + } + } + } + } + } + ]); + }); + + test("should return a collapsed range with no parent when the document has no AST", async () => { + await client.writeDocument("test.json", ``); + const uri = await client.openDocument("test.json"); + + const result = await client.sendRequest(SelectionRangeRequest.type, { + textDocument: { uri }, + positions: [{ line: 0, character: 0 }] + }); + + expect(result).toEqual([ + { + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 } + } + } + ]); + }); +}); diff --git a/language-server/src/features/SelectionRanges.ts b/language-server/src/features/SelectionRanges.ts new file mode 100644 index 0000000..8c92f72 --- /dev/null +++ b/language-server/src/features/SelectionRanges.ts @@ -0,0 +1,89 @@ +import * as jsonc from "jsonc-parser"; + +import type { SelectionRange, ServerCapabilities } from "vscode-languageserver"; +import type { Position, Range } from "vscode-languageserver-textdocument"; +import type { Server } from "../services/Server.ts"; +import type { JsonDocuments } from "../services/JsonDocuments.ts"; +import type { JsonDocument } from "../models/JsonDocument.ts"; + +export class SelectionRanges { + private jsonDocuments: JsonDocuments; + + constructor(server: Server, jsonDocuments: JsonDocuments) { + this.jsonDocuments = jsonDocuments; + + server.onInitialize(() => { + const serverCapabilities: ServerCapabilities = { + selectionRangeProvider: true + }; + + return { + capabilities: serverCapabilities + }; + }); + + server.onSelectionRanges((params) => { + const jsonDocument = this.jsonDocuments.get(params.textDocument.uri)!; + return params.positions.map((position) => getSelectionRange(jsonDocument, position)); + }); + } +} + +const getSelectionRange = (jsonDocument: JsonDocument, position: Position): SelectionRange => { + const offset = jsonDocument.offsetAt(position); + let node = jsonDocument.findNodeAtPosition(position); + + const scanner = jsonc.createScanner(jsonDocument.getText(), true); + const ranges: Range[] = []; + + while (node) { + switch (node.type) { + case "string": + case "object": + case "array": { + const innerStart = node.offset + 1; + const innerEnd = node.offset + node.length - 1; + if (innerStart < innerEnd && offset >= innerStart && offset <= innerEnd) { + ranges.push(newRange(jsonDocument, innerStart, innerEnd)); + } + ranges.push(newRange(jsonDocument, node.offset, node.offset + node.length)); + break; + } + case "number": + case "boolean": + case "null": + case "property": + ranges.push(newRange(jsonDocument, node.offset, node.offset + node.length)); + break; + } + + if (node.type === "property" || (node.parent && node.parent.type === "array")) { + const afterComma = offsetAfterMatchingToken(scanner, node.offset + node.length, jsonc.SyntaxKind.CommaToken); + if (afterComma !== -1) { + ranges.push(newRange(jsonDocument, node.offset, afterComma)); + } + } + + node = node.parent; + } + + let current: SelectionRange | undefined; + for (let i = ranges.length - 1; i >= 0; i--) { + current = { range: ranges[i], parent: current }; + } + + return current ?? { range: { start: position, end: position } }; +}; + +const newRange = (jsonDocument: JsonDocument, start: number, end: number): Range => ({ + start: jsonDocument.positionAt(start), + end: jsonDocument.positionAt(end) +}); + +const offsetAfterMatchingToken = (scanner: jsonc.JSONScanner, fromOffset: number, expectedToken: jsonc.SyntaxKind): number => { + scanner.setPosition(fromOffset); + if (scanner.scan() === expectedToken) { + return scanner.getTokenOffset() + scanner.getTokenLength(); + } + return -1; +}; From 9bdec57f5b7b7c9047e5a3f3846d2b685f0ce82e Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 14 Aug 2026 13:52:02 -0700 Subject: [PATCH 2/5] Add rangeAt method to JsonDocument --- language-server/src/features/Formatting.ts | 5 +---- language-server/src/features/SchemaValidation.ts | 10 ++-------- language-server/src/features/SelectionRanges.ts | 13 ++++--------- language-server/src/features/SyntaxValidation.ts | 5 +---- language-server/src/models/JsonDocument.ts | 7 +++++++ 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/language-server/src/features/Formatting.ts b/language-server/src/features/Formatting.ts index 8455cd9..4ed5c72 100644 --- a/language-server/src/features/Formatting.ts +++ b/language-server/src/features/Formatting.ts @@ -57,10 +57,7 @@ export class Formatting { }); return edits.map((edit) => ({ - range: { - start: jsonDocument.positionAt(edit.offset), - end: jsonDocument.positionAt(edit.offset + edit.length) - }, + range: jsonDocument.rangeAt(edit.offset, edit.offset + edit.length), newText: edit.content })); } catch (error: unknown) { diff --git a/language-server/src/features/SchemaValidation.ts b/language-server/src/features/SchemaValidation.ts index 2299a6f..f484e6a 100644 --- a/language-server/src/features/SchemaValidation.ts +++ b/language-server/src/features/SchemaValidation.ts @@ -20,10 +20,7 @@ export class SchemaValidation implements DiagnosticsProvider { if (node) { schemaDiagnostics.push({ severity: DiagnosticSeverity.Error, - range: { - start: jsonDocument.positionAt(node.offset), - end: jsonDocument.positionAt(node.offset + node.length) - }, + range: jsonDocument.rangeAt(node.offset, node.offset + node.length), message: formatError(error), source: "hyperjump-json-language-server" }); @@ -35,10 +32,7 @@ export class SchemaValidation implements DiagnosticsProvider { if (schemaNode) { schemaDiagnostics.push({ severity: DiagnosticSeverity.Error, - range: { - start: jsonDocument.positionAt(schemaNode.offset), - end: jsonDocument.positionAt(schemaNode.offset + schemaNode.length) - }, + range: jsonDocument.rangeAt(schemaNode.offset, schemaNode.offset + schemaNode.length), message: error instanceof Error ? error.message : String(error), source: "hyperjump-json-language-server" }); diff --git a/language-server/src/features/SelectionRanges.ts b/language-server/src/features/SelectionRanges.ts index 8c92f72..2b3d5f1 100644 --- a/language-server/src/features/SelectionRanges.ts +++ b/language-server/src/features/SelectionRanges.ts @@ -44,23 +44,23 @@ const getSelectionRange = (jsonDocument: JsonDocument, position: Position): Sele const innerStart = node.offset + 1; const innerEnd = node.offset + node.length - 1; if (innerStart < innerEnd && offset >= innerStart && offset <= innerEnd) { - ranges.push(newRange(jsonDocument, innerStart, innerEnd)); + ranges.push(jsonDocument.rangeAt(innerStart, innerEnd)); } - ranges.push(newRange(jsonDocument, node.offset, node.offset + node.length)); + ranges.push(jsonDocument.rangeAt(node.offset, node.offset + node.length)); break; } case "number": case "boolean": case "null": case "property": - ranges.push(newRange(jsonDocument, node.offset, node.offset + node.length)); + ranges.push(jsonDocument.rangeAt(node.offset, node.offset + node.length)); break; } if (node.type === "property" || (node.parent && node.parent.type === "array")) { const afterComma = offsetAfterMatchingToken(scanner, node.offset + node.length, jsonc.SyntaxKind.CommaToken); if (afterComma !== -1) { - ranges.push(newRange(jsonDocument, node.offset, afterComma)); + ranges.push(jsonDocument.rangeAt(node.offset, afterComma)); } } @@ -75,11 +75,6 @@ const getSelectionRange = (jsonDocument: JsonDocument, position: Position): Sele return current ?? { range: { start: position, end: position } }; }; -const newRange = (jsonDocument: JsonDocument, start: number, end: number): Range => ({ - start: jsonDocument.positionAt(start), - end: jsonDocument.positionAt(end) -}); - const offsetAfterMatchingToken = (scanner: jsonc.JSONScanner, fromOffset: number, expectedToken: jsonc.SyntaxKind): number => { scanner.setPosition(fromOffset); if (scanner.scan() === expectedToken) { diff --git a/language-server/src/features/SyntaxValidation.ts b/language-server/src/features/SyntaxValidation.ts index 1dffe8e..0fdb692 100644 --- a/language-server/src/features/SyntaxValidation.ts +++ b/language-server/src/features/SyntaxValidation.ts @@ -8,10 +8,7 @@ export class SyntaxValidation implements DiagnosticsProvider { async getDiagnostics(jsonDocument: JsonDocument) { return jsonDocument.getParseErrors().map((error) => ({ severity: DiagnosticSeverity.Error, - range: { - start: jsonDocument.positionAt(error.offset), - end: jsonDocument.positionAt(error.offset + error.length) - }, + range: jsonDocument.rangeAt(error.offset, error.offset + error.length), message: jsonc.printParseErrorCode(error.error), source: "hyperjump-json-language-server" })); diff --git a/language-server/src/models/JsonDocument.ts b/language-server/src/models/JsonDocument.ts index cd7565d..f6d4bf0 100644 --- a/language-server/src/models/JsonDocument.ts +++ b/language-server/src/models/JsonDocument.ts @@ -105,6 +105,13 @@ export class JsonDocument implements TextDocument { return this.textDocument.offsetAt(position); } + rangeAt(startOffset: number, endOffset: number) { + return { + start: this.positionAt(startOffset), + end: this.positionAt(endOffset) + }; + } + update(changes: TextDocumentContentChangeEvent[], version: number) { TextDocument.update(this.textDocument, changes, version); this.validate(); From 9144dd4d8bf7176391325463399449762ae8b61f Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 14 Aug 2026 22:11:21 -0700 Subject: [PATCH 3/5] Fix a couple of tests --- language-server/src/features/SelectionRanges.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/language-server/src/features/SelectionRanges.test.ts b/language-server/src/features/SelectionRanges.test.ts index 793b29e..8c215a9 100644 --- a/language-server/src/features/SelectionRanges.test.ts +++ b/language-server/src/features/SelectionRanges.test.ts @@ -317,7 +317,7 @@ describe("SelectionRanges", () => { }); test("should return a collapsed range with no parent for a position at the very end of the document", async () => { - await client.writeDocument("test.json", `{"a": 1}`); + await client.writeDocument("test.json", `{"a": 1}\n`); const uri = await client.openDocument("test.json"); const result = await client.sendRequest(SelectionRangeRequest.type, { @@ -341,7 +341,7 @@ describe("SelectionRanges", () => { const result = await client.sendRequest(SelectionRangeRequest.type, { textDocument: { uri }, - positions: [{ line: 0, character: 6 }] + positions: [{ line: 0, character: 7 }] }); expect(result).toEqual([ From 606c3edad76e568a265fdd714dc7940fc4bc5435 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 14 Aug 2026 22:20:16 -0700 Subject: [PATCH 4/5] A little cleanup --- language-server/src/features/SelectionRanges.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/language-server/src/features/SelectionRanges.ts b/language-server/src/features/SelectionRanges.ts index 2b3d5f1..2af50fc 100644 --- a/language-server/src/features/SelectionRanges.ts +++ b/language-server/src/features/SelectionRanges.ts @@ -43,7 +43,7 @@ const getSelectionRange = (jsonDocument: JsonDocument, position: Position): Sele case "array": { const innerStart = node.offset + 1; const innerEnd = node.offset + node.length - 1; - if (innerStart < innerEnd && offset >= innerStart && offset <= innerEnd) { + if (innerStart < innerEnd && offset >= innerStart) { ranges.push(jsonDocument.rangeAt(innerStart, innerEnd)); } ranges.push(jsonDocument.rangeAt(node.offset, node.offset + node.length)); @@ -57,7 +57,7 @@ const getSelectionRange = (jsonDocument: JsonDocument, position: Position): Sele break; } - if (node.type === "property" || (node.parent && node.parent.type === "array")) { + if (node.type === "property" || (node.parent?.type === "array")) { const afterComma = offsetAfterMatchingToken(scanner, node.offset + node.length, jsonc.SyntaxKind.CommaToken); if (afterComma !== -1) { ranges.push(jsonDocument.rangeAt(node.offset, afterComma)); From b7c4cb1a9e06338acbacfe3351cd8f93cfd6585a Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 14 Aug 2026 22:18:39 -0700 Subject: [PATCH 5/5] Refactor --- .../src/features/SelectionRanges.ts | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/language-server/src/features/SelectionRanges.ts b/language-server/src/features/SelectionRanges.ts index 2af50fc..2f36284 100644 --- a/language-server/src/features/SelectionRanges.ts +++ b/language-server/src/features/SelectionRanges.ts @@ -1,7 +1,6 @@ import * as jsonc from "jsonc-parser"; import type { SelectionRange, ServerCapabilities } from "vscode-languageserver"; -import type { Position, Range } from "vscode-languageserver-textdocument"; import type { Server } from "../services/Server.ts"; import type { JsonDocuments } from "../services/JsonDocuments.ts"; import type { JsonDocument } from "../models/JsonDocument.ts"; @@ -24,61 +23,53 @@ export class SelectionRanges { server.onSelectionRanges((params) => { const jsonDocument = this.jsonDocuments.get(params.textDocument.uri)!; - return params.positions.map((position) => getSelectionRange(jsonDocument, position)); + const scanner = jsonc.createScanner(jsonDocument.getText(), true); + + return params.positions.map((position) => { + const offset = jsonDocument.offsetAt(position); + const node = jsonDocument.findNodeAtPosition(position); + + if (!node) { + return { range: { start: position, end: position } }; + } + + return this.buildSelectionRange(node, jsonDocument, scanner, offset); + }); }); } -} -const getSelectionRange = (jsonDocument: JsonDocument, position: Position): SelectionRange => { - const offset = jsonDocument.offsetAt(position); - let node = jsonDocument.findNodeAtPosition(position); + private buildSelectionRange(node: jsonc.Node, jsonDocument: JsonDocument, scanner: jsonc.JSONScanner, offset: number): SelectionRange { + let selection = node.parent ? this.buildSelectionRange(node.parent, jsonDocument, scanner, offset) : undefined; - const scanner = jsonc.createScanner(jsonDocument.getText(), true); - const ranges: Range[] = []; + if (node.type === "property" || (node.parent?.type === "array")) { + scanner.setPosition(node.offset + node.length); + if (scanner.scan() === jsonc.SyntaxKind.CommaToken) { + const afterComma = scanner.getTokenOffset() + scanner.getTokenLength(); + selection = { range: jsonDocument.rangeAt(node.offset, afterComma), parent: selection }; + } + } - while (node) { switch (node.type) { case "string": case "object": case "array": { + selection = { range: jsonDocument.rangeAt(node.offset, node.offset + node.length), parent: selection }; + const innerStart = node.offset + 1; const innerEnd = node.offset + node.length - 1; if (innerStart < innerEnd && offset >= innerStart) { - ranges.push(jsonDocument.rangeAt(innerStart, innerEnd)); + selection = { range: jsonDocument.rangeAt(innerStart, innerEnd), parent: selection }; } - ranges.push(jsonDocument.rangeAt(node.offset, node.offset + node.length)); break; } case "number": case "boolean": case "null": case "property": - ranges.push(jsonDocument.rangeAt(node.offset, node.offset + node.length)); + selection = { range: jsonDocument.rangeAt(node.offset, node.offset + node.length), parent: selection }; break; } - if (node.type === "property" || (node.parent?.type === "array")) { - const afterComma = offsetAfterMatchingToken(scanner, node.offset + node.length, jsonc.SyntaxKind.CommaToken); - if (afterComma !== -1) { - ranges.push(jsonDocument.rangeAt(node.offset, afterComma)); - } - } - - node = node.parent; - } - - let current: SelectionRange | undefined; - for (let i = ranges.length - 1; i >= 0; i--) { - current = { range: ranges[i], parent: current }; + return selection; } - - return current ?? { range: { start: position, end: position } }; -}; - -const offsetAfterMatchingToken = (scanner: jsonc.JSONScanner, fromOffset: number, expectedToken: jsonc.SyntaxKind): number => { - scanner.setPosition(fromOffset); - if (scanner.scan() === expectedToken) { - return scanner.getTokenOffset() + scanner.getTokenLength(); - } - return -1; -}; +}