From 9ca0e45f8d5df24a756fd7c29d2830e20249efd5 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 28 Jan 2019 14:41:27 +0100 Subject: [PATCH] support semantic selection (for Microsoft/vscode#67232) --- CHANGELOG.md | 4 + README.md | 1 + src/jsonLanguageService.ts | 5 +- src/services/jsonSelectionRanges.ts | 69 ++++++++++++ src/test/selectionRanges.test.ts | 167 ++++++++++++++++++++++++++++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/services/jsonSelectionRanges.ts create mode 100644 src/test/selectionRanges.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ba98637e..47b36931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +NEXT +================== +* New API `LanguageService.getSelectionRanges` to get semantic selection ranges. + 3.2.0 / 2018-09-27 ================== * New API `LanguageServiceParams.ClientCapabilities` to define what LSP capabilities the client supports. diff --git a/README.md b/README.md index dd7ad421..4b759228 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ and the Monaco editor. - *findDocumentColors* provides all color symbols in the given document, *getColorPresentations* returns available color formats for a color symbol. - *format* formats the code at the given range. - *getFoldingRanges* gets folding ranges for the given document + - *getSelectionRanges* gets selection ranges for a given location. - use *parseJSONDocument* create a JSON document from source code, or *newJSONDocument* to create the document from an AST. diff --git a/src/jsonLanguageService.ts b/src/jsonLanguageService.ts index aa58c4aa..ca553b0d 100644 --- a/src/jsonLanguageService.ts +++ b/src/jsonLanguageService.ts @@ -18,6 +18,7 @@ import { parse as parseJSON, JSONDocument as InternalJSONDocument, newJSONDocume import { schemaContributions } from './services/configuration'; import { JSONSchemaService } from './services/jsonSchemaService'; import { getFoldingRanges } from './services/jsonFolding'; +import { getSelectionRanges } from './services/jsonSelectionRanges'; import { format as formatJSON } from 'jsonc-parser'; import { @@ -53,6 +54,7 @@ export interface LanguageService { doHover(document: TextDocument, position: Position, doc: JSONDocument): Thenable; format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[]; getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number }): FoldingRange[]; + getSelectionRanges(document: TextDocument, position: Position, doc: JSONDocument): Range[]; } @@ -89,7 +91,8 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi findDocumentColors: jsonDocumentSymbols.findDocumentColors.bind(jsonDocumentSymbols), getColorPresentations: jsonDocumentSymbols.getColorPresentations.bind(jsonDocumentSymbols), doHover: jsonHover.doHover.bind(jsonHover), - getFoldingRanges: getFoldingRanges, + getFoldingRanges, + getSelectionRanges, format: (d, r, o) => { let range = void 0; if (r) { diff --git a/src/services/jsonSelectionRanges.ts b/src/services/jsonSelectionRanges.ts new file mode 100644 index 00000000..b4a2938f --- /dev/null +++ b/src/services/jsonSelectionRanges.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Range, Position, TextDocument } from 'vscode-languageserver-types'; +import { JSONDocument } from '../parser/jsonParser'; +import { SyntaxKind, createScanner } from 'jsonc-parser'; +import { notEqual } from 'assert'; + +export function getSelectionRanges(document: TextDocument, position: Position, doc: JSONDocument): Range[] { + let offset = document.offsetAt(position); + let node = doc.getNodeFromOffset(offset, true); + if (!node) { + return []; + } + const scanner = createScanner(document.getText(), true); + const result: Range[] = []; + + + + while (node) { + switch (node.type) { + case 'string': + case 'object': + case 'array': + // range without ", [ or { + const cStart = node.offset + 1, cEnd = node.offset + node.length - 1; + if (cStart < cEnd && offset >= cStart && offset <= cEnd) { + result.push(newRange(cStart, cEnd)); + } + result.push(newRange(node.offset, node.offset + node.length)); + break; + case 'number': + case 'boolean': + case 'null': + case 'property': + result.push(newRange(node.offset, node.offset + node.length)); + break; + } + if (node.type === 'property' || node.parent && node.parent.type === 'array') { + const afterCommaOffset = getOffsetAfterNextToken(node.offset + node.length, SyntaxKind.CommaToken); + if (afterCommaOffset !== -1) { + result.push(newRange(node.offset, afterCommaOffset)); + } + } + node = node.parent; + } + return result; + + function newRange(start: number, end: number) { + return Range.create(document.positionAt(start), document.positionAt(end)); + } + + function getOffsetAfterNextToken(offset: number, expectedToken: SyntaxKind): number { + scanner.setPosition(offset); + let token = scanner.scan(); + if (token === expectedToken) { + return scanner.getTokenOffset() + scanner.getTokenLength(); + } + return -1; + } +} + + + + + diff --git a/src/test/selectionRanges.test.ts b/src/test/selectionRanges.test.ts new file mode 100644 index 00000000..4a17a2d4 --- /dev/null +++ b/src/test/selectionRanges.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'mocha'; +import * as assert from 'assert'; +import { TextDocument } from 'vscode-languageserver-types'; +import { getLanguageService } from '../jsonLanguageService'; + +function assertRanges(content: string, expected: (number | string)[][]): void { + let message = `Test ${content}`; + + let offset = content.indexOf('|'); + content = content.substr(0, offset) + content.substr(offset + 1); + + const ls = getLanguageService({}); + + const document = TextDocument.create('test://foo.json', 'json', 1, content); + const jsonDoc = ls.parseJSONDocument(document); + + const actualRanges = ls.getSelectionRanges(document, document.positionAt(offset), jsonDoc); + const offsetPairs = actualRanges.map(r => { + return [document.offsetAt(r.start), document.getText(r)]; + }); + + message += `\n${JSON.stringify(offsetPairs)} should equal to ${JSON.stringify(expected)}`; + assert.deepEqual(offsetPairs, expected, message); +} + +suite('JSON SelectionRange', () => { + + test('Strings', () => { + assertRanges('"ab|cd"', [ + [1, 'abcd'], + [0, '"abcd"'] + ]); + assertRanges('"|abcd"', [ + [1, 'abcd'], + [0, '"abcd"'] + ]); + assertRanges('"abcd|"', [ + [1, 'abcd'], + [0, '"abcd"'] + ]); + assertRanges('|"abcd"', [ + [0, '"abcd"'] + ]); + assertRanges('"abcd"|', [ + [0, '"abcd"'] + ]); + }); + test('Bools, Numbers and Nulls', () => { + assertRanges('|true', [ + [0, 'true'] + ]); + assertRanges('false|', [ + [0, 'false'] + ]); + assertRanges('fal|se', [ + [0, 'false'] + ]); + assertRanges('|1', [ + [0, '1'] + ]); + assertRanges('5677|', [ + [0, '5677'] + ]); + assertRanges('567.|7', [ + [0, '567.7'] + ]); + assertRanges('|null', [ + [0, 'null'] + ]); + assertRanges('null|', [ + [0, 'null'] + ]); + assertRanges('nu|ll', [ + [0, 'null'] + ]); + }); + test('Properties', () => { + assertRanges('{ "f|oo": true, "bar": "bit" }', [ + [3, 'foo'], + [2, '"foo"'], + [2, '"foo": true'], + [2, '"foo": true,'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo"|: true, "bar": "bit" }', [ + [2, '"foo"'], + [2, '"foo": true'], + [2, '"foo": true,'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo":| true, "bar": "bit" }', [ + [2, '"foo": true'], + [2, '"foo": true,'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo": |true, "bar": "bit" }', [ + [9, 'true'], + [2, '"foo": true'], + [2, '"foo": true,'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo": true|, "bar": "bit" }', [ + [9, 'true'], + [2, '"foo": true'], + [2, '"foo": true,'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo": true,| "bar": "bit" }', [ + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo": true, |"bar": "bit" }', [ + [15, '"bar"'], + [15, '"bar": "bit"'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + assertRanges('{ "foo": true, "bar": "bit"| }', [ + [22, '"bit"'], + [15, '"bar": "bit"'], + [1, ' "foo": true, "bar": "bit" '], + [0, '{ "foo": true, "bar": "bit" }'], + ]); + }); + test('Objects', () => { + assertRanges('|{}', [ + [0, '{}'] + ]); + assertRanges('{|}', [ + [0, '{}'] + ]); + assertRanges('{| }', [ + [1, ' '], + [0, '{ }'] + ]); + }); + test('Array', () => { + assertRanges('|[[ ], []]', [ + [0, '[[ ], []]'] + ]); + assertRanges('[|[ ], []]', [ + [1, '[ ]'], + [1, '[ ],'], + [1, '[ ], []'], + [0, '[[ ], []]'] + ]); + assertRanges('[[| ], []]', [ + [2, ' '], + [1, '[ ]'], + [1, '[ ],'], + [1, '[ ], []'], + [0, '[[ ], []]'] + ]); + }); +});