Skip to content

Commit

Permalink
support semantic selection (for microsoft/vscode#67232)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeschli committed Jan 28, 2019
1 parent c4f4431 commit 9ca0e45
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 4 additions & 1 deletion src/jsonLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,6 +54,7 @@ export interface LanguageService {
doHover(document: TextDocument, position: Position, doc: JSONDocument): Thenable<Hover | null>;
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
getFoldingRanges(document: TextDocument, context?: { rangeLimit?: number }): FoldingRange[];
getSelectionRanges(document: TextDocument, position: Position, doc: JSONDocument): Range[];
}


Expand Down Expand Up @@ -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) {
Expand Down
69 changes: 69 additions & 0 deletions src/services/jsonSelectionRanges.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}





167 changes: 167 additions & 0 deletions src/test/selectionRanges.test.ts
Original file line number Diff line number Diff line change
@@ -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, '[[ ], []]']
]);
});
});

0 comments on commit 9ca0e45

Please sign in to comment.