diff --git a/extensions/json/server/package.json b/extensions/json/server/package.json index 672a639db0f..fad172aa181 100644 --- a/extensions/json/server/package.json +++ b/extensions/json/server/package.json @@ -16,7 +16,8 @@ "vscode-uri": "^1.0.3" }, "devDependencies": { - "@types/node": "7.0.43" + "@types/node": "7.0.43", + "@types/mocha": "2.2.33" }, "scripts": { "compile": "gulp compile-extension:json-server", @@ -24,6 +25,7 @@ "install-service-next": "yarn add vscode-json-languageservice@next", "install-service-local": "yarn link vscode-json-languageservice", "install-server-next": "yarn add vscode-languageserver@next", - "install-server-local": "yarn link vscode-languageserver-server" + "install-server-local": "yarn link vscode-languageserver-server", + "test": "npm run compile && ../../../node_modules/.bin/mocha" } } diff --git a/extensions/json/server/src/folding.ts b/extensions/json/server/src/folding.ts new file mode 100644 index 00000000000..958e2f7388a --- /dev/null +++ b/extensions/json/server/src/folding.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TextDocument, Position } from 'vscode-languageserver'; +import { createScanner, SyntaxKind, ScanError } from 'jsonc-parser'; +import { FoldingRangeType, FoldingRange, FoldingRangeList } from './protocol/foldingProvider.proposed'; + +export function getFoldingRegions(document: TextDocument) { + let ranges: FoldingRange[] = []; + let stack: FoldingRange[] = []; + let prevStart = -1; + let scanner = createScanner(document.getText(), false); + let token = scanner.scan(); + while (token !== SyntaxKind.EOF) { + switch (token) { + case SyntaxKind.OpenBraceToken: + case SyntaxKind.OpenBracketToken: { + let startLine = document.positionAt(scanner.getTokenOffset()).line; + let range = { startLine, endLine: startLine, type: token === SyntaxKind.OpenBraceToken ? 'object' : 'array' }; + stack.push(range); + break; + } + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CloseBracketToken: { + let type = token === SyntaxKind.CloseBraceToken ? 'object' : 'array'; + if (stack.length > 0 && stack[stack.length - 1].type === type) { + let range = stack.pop(); + let line = document.positionAt(scanner.getTokenOffset()).line; + if (range && line > range.startLine + 1 && prevStart !== range.startLine) { + range.endLine = line - 1; + ranges.push(range); + prevStart = range.startLine; + } + } + break; + } + + case SyntaxKind.BlockCommentTrivia: { + let startLine = document.positionAt(scanner.getTokenOffset()).line; + let endLine = document.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()).line; + if (scanner.getTokenError() === ScanError.UnexpectedEndOfComment && startLine + 1 < document.lineCount) { + scanner.setPosition(document.offsetAt(Position.create(startLine + 1, 0))); + } else { + if (startLine < endLine) { + ranges.push({ startLine, endLine, type: FoldingRangeType.Comment }); + prevStart = startLine; + } + } + break; + } + + case SyntaxKind.LineCommentTrivia: { + let text = document.getText().substr(scanner.getTokenOffset(), scanner.getTokenLength()); + let m = text.match(/^\/\/\s*#(region\b)|(endregion\b)/); + if (m) { + let line = document.positionAt(scanner.getTokenOffset()).line; + if (m[1]) { // start pattern match + let range = { startLine: line, endLine: line, type: FoldingRangeType.Region }; + stack.push(range); + } else { + let i = stack.length - 1; + while (i >= 0 && stack[i].type !== FoldingRangeType.Region) { + i--; + } + if (i >= 0) { + let range = stack[i]; + stack.length = i; + if (line > range.startLine && prevStart !== range.startLine) { + range.endLine = line; + ranges.push(range); + prevStart = range.startLine; + } + } + } + } + break; + } + + } + token = scanner.scan(); + } + return { ranges }; +} \ No newline at end of file diff --git a/extensions/json/server/src/jsonServerMain.ts b/extensions/json/server/src/jsonServerMain.ts index fa77f9a039e..67160f689e5 100644 --- a/extensions/json/server/src/jsonServerMain.ts +++ b/extensions/json/server/src/jsonServerMain.ts @@ -7,20 +7,20 @@ import { createConnection, IConnection, TextDocuments, TextDocument, InitializeParams, InitializeResult, NotificationType, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities, DocumentColorRequest, ColorPresentationRequest, Position, + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, DocumentColorRequest, ColorPresentationRequest } from 'vscode-languageserver'; import { xhr, XHRResponse, configure as configureHttpRequests, getErrorStatusDescription } from 'request-light'; -import fs = require('fs'); +import * as fs from 'fs'; import URI from 'vscode-uri'; import * as URL from 'url'; -import Strings = require('./utils/strings'); +import { startsWith } from './utils/strings'; import { formatError, runSafe, runSafeAsync } from './utils/errors'; import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; -import { createScanner, SyntaxKind, ScanError } from 'jsonc-parser'; +import { getFoldingRegions } from './folding'; -import { FoldingRangeType, FoldingRangesRequest, FoldingRange, FoldingRangeList, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed'; +import { FoldingRangesRequest, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed'; interface ISchemaAssociations { [pattern: string]: string[]; @@ -93,14 +93,14 @@ let workspaceContext = { }; let schemaRequestService = (uri: string): Thenable => { - if (Strings.startsWith(uri, 'file://')) { + if (startsWith(uri, 'file://')) { let fsPath = URI.parse(uri).fsPath; return new Promise((c, e) => { fs.readFile(fsPath, 'UTF-8', (err, result) => { err ? e('') : c(result.toString()); }); }); - } else if (Strings.startsWith(uri, 'vscode://')) { + } else if (startsWith(uri, 'vscode://')) { return connection.sendRequest(VSCodeContentRequest.type, uri).then(responseText => { return responseText; }, error => { @@ -361,80 +361,7 @@ connection.onRequest(FoldingRangesRequest.type, params => { return runSafe(() => { let document = documents.get(params.textDocument.uri); if (document) { - let ranges: FoldingRange[] = []; - let stack: FoldingRange[] = []; - let prevStart = -1; - let scanner = createScanner(document.getText(), false); - let token = scanner.scan(); - while (token !== SyntaxKind.EOF) { - switch (token) { - case SyntaxKind.OpenBraceToken: - case SyntaxKind.OpenBracketToken: { - let startLine = document.positionAt(scanner.getTokenOffset()).line; - let range = { startLine, endLine: startLine, type: token === SyntaxKind.OpenBraceToken ? 'object' : 'array' }; - stack.push(range); - break; - } - case SyntaxKind.CloseBraceToken: - case SyntaxKind.CloseBracketToken: { - let type = token === SyntaxKind.CloseBraceToken ? 'object' : 'array'; - if (stack.length > 0 && stack[stack.length - 1].type === type) { - let range = stack.pop(); - let line = document.positionAt(scanner.getTokenOffset()).line; - if (range && line > range.startLine + 1 && prevStart !== range.startLine) { - range.endLine = line - 1; - ranges.push(range); - prevStart = range.startLine; - } - } - break; - } - - case SyntaxKind.BlockCommentTrivia: { - let startLine = document.positionAt(scanner.getTokenOffset()).line; - let endLine = document.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()).line; - if (scanner.getTokenError() === ScanError.UnexpectedEndOfComment && startLine + 1 < document.lineCount) { - scanner.setPosition(document.offsetAt(Position.create(startLine + 1, 0))); - } else { - if (startLine < endLine) { - ranges.push({ startLine, endLine, type: FoldingRangeType.Comment }); - prevStart = startLine; - } - } - break; - } - - case SyntaxKind.LineCommentTrivia: { - let text = document.getText().substr(scanner.getTokenOffset(), scanner.getTokenLength()); - let m = text.match(/^\/\/\s*#(region\b)|(endregion\b)/); - if (m) { - let line = document.positionAt(scanner.getTokenOffset()).line; - if (m[1]) { // start pattern match - let range = { startLine: line, endLine: line, type: FoldingRangeType.Region }; - stack.push(range); - } else { - let i = stack.length - 1; - while (i >= 0 && stack[i].type !== FoldingRangeType.Region) { - i--; - } - if (i >= 0) { - let range = stack[i]; - stack.length = i; - if (line > range.startLine && prevStart !== range.startLine) { - range.endLine = line; - ranges.push(range); - prevStart = range.startLine; - } - } - } - } - break; - } - - } - token = scanner.scan(); - } - return { ranges }; + return getFoldingRegions(document); } return null; }, null, `Error while computing folding ranges for ${params.textDocument.uri}`); diff --git a/extensions/json/server/src/test/folding.test.ts b/extensions/json/server/src/test/folding.test.ts new file mode 100644 index 00000000000..0ef69577cc0 --- /dev/null +++ b/extensions/json/server/src/test/folding.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { getFoldingRegions } from '../folding'; + +interface ExpectedIndentRange { + startLine: number; + endLine: number; + type?: string; +} + +function assertRanges(lines: string[], expected: ExpectedIndentRange[]): void { + let document = TextDocument.create('test://foo/bar.json', 'json', 1, lines.join('\n')); + let actual = getFoldingRegions(document).ranges; + + + let actualRanges = []; + for (let i = 0; i < actual.length; i++) { + actualRanges[i] = r(actual[i].startLine, actual[i].endLine, actual[i].type); + } + actualRanges = actualRanges.sort((r1, r2) => r1.startLine - r2.startLine); + assert.deepEqual(actualRanges, expected); +} + +function r(startLine: number, endLine: number, type?: string): ExpectedIndentRange { + return { startLine, endLine, type }; +} + +suite('Object Folding', () => { + test('Fold one level', () => { + let range = [ + /*0*/'{', + /*1*/'"foo":"bar"', + /*2*/'}' + ]; + assertRanges(range, [r(0, 1, 'object')]); + }); + + test('Fold two level', () => { + let range = [ + /*0*/'[', + /*1*/'{', + /*2*/'"foo":"bar"', + /*3*/'}', + /*4*/']' + ]; + assertRanges(range, [r(0, 3, 'array'), r(1, 2, 'object')]); + }); + + test('Fold Arrays', () => { + let range = [ + /*0*/'[', + /*1*/'[', + /*2*/'],[', + /*3*/'1', + /*4*/']', + /*5*/']' + ]; + assertRanges(range, [r(0, 4, 'array'), r(2, 3, 'array')]); + }); + + test('Filter start on same line', () => { + let range = [ + /*0*/'[[', + /*1*/'[', + /*2*/'],[', + /*3*/'1', + /*4*/']', + /*5*/']]' + ]; + assertRanges(range, [r(0, 4, 'array'), r(2, 3, 'array')]); + }); + + test('Fold commment', () => { + let range = [ + /*0*/'/*', + /*1*/' multi line', + /*2*/'*/', + ]; + assertRanges(range, [r(0, 2, 'comment')]); + }); + + test('Incomplete commment', () => { + let range = [ + /*0*/'/*', + /*1*/'{', + /*2*/'"foo":"bar"', + /*3*/'}', + ]; + assertRanges(range, [r(1, 2, 'object')]); + }); + + test('Fold regions', () => { + let range = [ + /*0*/'// #region', + /*1*/'{', + /*2*/'}', + /*3*/'// #endregion', + ]; + assertRanges(range, [r(0, 3, 'region')]); + }); + +}); diff --git a/extensions/json/server/test/mocha.opts b/extensions/json/server/test/mocha.opts new file mode 100644 index 00000000000..d3e46273a0c --- /dev/null +++ b/extensions/json/server/test/mocha.opts @@ -0,0 +1,3 @@ +--ui tdd +--useColors true +./out/test/**/*.test.js \ No newline at end of file diff --git a/extensions/json/server/yarn.lock b/extensions/json/server/yarn.lock index c1a1fdb01f3..26ced2f51ec 100644 --- a/extensions/json/server/yarn.lock +++ b/extensions/json/server/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/mocha@2.2.33": + version "2.2.33" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def" + "@types/node@7.0.43": version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c"