diff --git a/extensions/json/server/src/jsonCompletion.ts b/extensions/json/server/src/jsonCompletion.ts index 60b9832bc20..93d172a7c3a 100644 --- a/extensions/json/server/src/jsonCompletion.ts +++ b/extensions/json/server/src/jsonCompletion.ts @@ -10,45 +10,52 @@ import Parser = require('./jsonParser'); import SchemaService = require('./jsonSchemaService'); import JsonSchema = require('./json-toolbox/jsonSchema'); import nls = require('./utils/nls'); +import {IJSONWorkerContribution} from './jsonContributions'; import {CompletionItem, CompletionItemKind, CompletionOptions, ITextDocument, TextDocumentIdentifier, TextDocumentPosition, Range, TextEdit} from 'vscode-languageserver'; export interface ISuggestionsCollector { add(suggestion: CompletionItem): void; - error(message: string): void; + error(message:string): void; + setAsIncomplete(): void; } export class JSONCompletion { private schemaService: SchemaService.IJSONSchemaService; + private contributions: IJSONWorkerContribution[]; - constructor(schemaService: SchemaService.IJSONSchemaService) { + constructor(schemaService: SchemaService.IJSONSchemaService, contributions: IJSONWorkerContribution[] = []) { this.schemaService = schemaService; + this.contributions = contributions; } public doSuggest(document: ITextDocument, textDocumentPosition: TextDocumentPosition, doc: Parser.JSONDocument): Thenable { - var offset = document.offsetAt(textDocumentPosition.position); - var node = doc.getNodeFromOffsetEndInclusive(offset); - - var overwriteRange = null; - var result: CompletionItem[] = []; + let offset = document.offsetAt(textDocumentPosition.position); + let node = doc.getNodeFromOffsetEndInclusive(offset); + + let overwriteRange = null; + let result: CompletionItem[] = []; if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) { overwriteRange = Range.create(document.positionAt(node.start), document.positionAt(node.end)); } - var proposed: { [key: string]: boolean } = {}; - var collector: ISuggestionsCollector = { + let proposed: { [key: string]: boolean } = {}; + let collector: ISuggestionsCollector = { add: (suggestion: CompletionItem) => { if (!proposed[suggestion.label]) { proposed[suggestion.label] = true; if (overwriteRange) { suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText); } - + result.push(suggestion); } + }, + setAsIncomplete: () => { + }, error: (message: string) => { console.log(message); @@ -56,17 +63,21 @@ export class JSONCompletion { }; return this.schemaService.getSchemaForResource(textDocumentPosition.uri, doc).then((schema) => { - var addValue = true; - var currentKey = ''; - var currentProperty: Parser.PropertyASTNode = null; + let collectionPromises: Thenable[] = []; + + let addValue = true; + let currentKey = ''; + let currentWord = ''; + let currentProperty: Parser.PropertyASTNode = null; if (node) { if (node.type === 'string') { - var stringNode = node; + let stringNode = node; if (stringNode.isKey) { addValue = !(node.parent && ((node.parent).value)); currentProperty = node.parent ? node.parent : null; - currentKey = document.getText().substr(node.start + 1, node.end - node.start - 1); + currentKey = document.getText().substring(node.start + 1, node.end - 1); + currentWord = document.getText().substring(node.start + 1, offset); if (node.parent) { node = node.parent.parent; } @@ -82,21 +93,29 @@ export class JSONCompletion { return result; } // don't suggest properties that are already present - var properties = (node).properties; + let properties = (node).properties; properties.forEach(p => { if (!currentProperty || currentProperty !== p) { proposed[p.key.value] = true; } }); - + + let isLast = properties.length === 0 || offset >= properties[properties.length - 1].start; if (schema) { // property proposals with schema - var isLast = properties.length === 0 || offset >= properties[properties.length - 1].start; this.getPropertySuggestions(schema, doc, node, currentKey, addValue, isLast, collector); } else if (node.parent) { // property proposals without schema this.getSchemaLessPropertySuggestions(doc, node, collector); } + + let location = node.getNodeLocation(); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectPropertySuggestions(textDocumentPosition.uri, location, this.getCurrentWord(document, offset), addValue, isLast, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); } @@ -112,23 +131,43 @@ export class JSONCompletion { // value proposals without schema this.getSchemaLessValueSuggestions(doc, node, offset, document, collector); } + if (!node) { + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectDefaultSuggestions(textDocumentPosition.uri, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } else { + if ((node.type === 'property') && offset > ( node).colonOffset) { + let parentKey = (node).key.value; - - - return result; + let valueNode = ( node).value; + if (!valueNode || offset <= valueNode.end) { + let location = node.parent.getNodeLocation(); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectValueSuggestions(textDocumentPosition.uri, location, parentKey, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } + } + } + return Promise.all(collectionPromises).then(() => result ); }); } private getPropertySuggestions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: Parser.ASTNode, currentWord: string, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector): void { - var matchingSchemas: Parser.IApplicableSchema[] = []; + let matchingSchemas: Parser.IApplicableSchema[] = []; doc.validate(schema.schema, matchingSchemas, node.start); matchingSchemas.forEach((s) => { if (s.node === node && !s.inverted) { - var schemaProperties = s.schema.properties; + let schemaProperties = s.schema.properties; if (schemaProperties) { Object.keys(schemaProperties).forEach((key: string) => { - var propertySchema = schemaProperties[key]; + let propertySchema = schemaProperties[key]; collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' }); }); } @@ -137,15 +176,15 @@ export class JSONCompletion { } private getSchemaLessPropertySuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, collector: ISuggestionsCollector): void { - var collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => { + let collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => { obj.properties.forEach((p) => { - var key = p.key.value; + let key = p.key.value; collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForSimilarProperty(key, p.value), documentation: '' }); }); }; if (node.parent.type === 'property') { // if the object is a property value, check the tree for other objects that hang under a property of the same name - var parentKey = (node.parent).key.value; + let parentKey = (node.parent).key.value; doc.visit((n) => { if (n.type === 'property' && (n).key.value === parentKey && (n).value && (n).value.type === 'object') { collectSuggestionsForSimilarObject((n).value); @@ -163,8 +202,8 @@ export class JSONCompletion { } private getSchemaLessValueSuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: ITextDocument, collector: ISuggestionsCollector): void { - var collectSuggestionsForValues = (value: Parser.ASTNode) => { - var content = this.getMatchingSnippet(value, document); + let collectSuggestionsForValues = (value: Parser.ASTNode) => { + let content = this.getMatchingSnippet(value, document); collector.add({ kind: this.getSuggestionKind(value.type), label: content, insertText: content, documentation: '' }); if (value.type === 'boolean') { this.addBooleanSuggestion(!value.getValue(), collector); @@ -176,12 +215,12 @@ export class JSONCompletion { collector.add({ kind: this.getSuggestionKind('array'), label: 'Empty array', insertText: '[\n\t{{}}\n]', documentation: '' }); } else { if (node.type === 'property' && offset > (node).colonOffset) { - var valueNode = (node).value; + let valueNode = (node).value; if (valueNode && offset > valueNode.end) { return; } // suggest values at the same key - var parentKey = (node).key.value; + let parentKey = (node).key.value; doc.visit((n) => { if (n.type === 'property' && (n).key.value === parentKey && (n).value) { collectSuggestionsForValues((n).value); @@ -192,7 +231,7 @@ export class JSONCompletion { if (node.type === 'array') { if (node.parent && node.parent.type === 'property') { // suggest items of an array at the same key - var parentKey = (node.parent).key.value; + let parentKey = (node.parent).key.value; doc.visit((n) => { if (n.type === 'property' && (n).key.value === parentKey && (n).value && (n).value.type === 'array') { (((n).value).items).forEach((n) => { @@ -217,9 +256,9 @@ export class JSONCompletion { if (!node) { this.addDefaultSuggestion(schema.schema, collector); } else { - var parentKey: string = null; + let parentKey: string = null; if (node && (node.type === 'property') && offset > (node).colonOffset) { - var valueNode = (node).value; + let valueNode = (node).value; if (valueNode && offset > valueNode.end) { return; // we are past the value node } @@ -227,7 +266,7 @@ export class JSONCompletion { node = node.parent; } if (node && (parentKey !== null || node.type === 'array')) { - var matchingSchemas: Parser.IApplicableSchema[] = []; + let matchingSchemas: Parser.IApplicableSchema[] = []; doc.validate(schema.schema, matchingSchemas, node.start); matchingSchemas.forEach((s) => { @@ -237,7 +276,7 @@ export class JSONCompletion { this.addEnumSuggestion(s.schema.items, collector); } if (s.schema.properties) { - var propertySchema = s.schema.properties[parentKey]; + let propertySchema = s.schema.properties[parentKey]; if (propertySchema) { this.addDefaultSuggestion(propertySchema, collector); this.addEnumSuggestion(propertySchema, collector); @@ -293,7 +332,7 @@ export class JSONCompletion { } private getLabelForValue(value: any): string { - var label = JSON.stringify(value); + let label = JSON.stringify(value); label = label.replace('{{', '').replace('}}', ''); if (label.length > 57) { return label.substr(0, 57).trim() + '...'; @@ -302,7 +341,7 @@ export class JSONCompletion { } private getSnippetForValue(value: any): string { - var snippet = JSON.stringify(value, null, '\t'); + let snippet = JSON.stringify(value, null, '\t'); switch (typeof value) { case 'object': if (value === null) { @@ -320,7 +359,7 @@ export class JSONCompletion { private getSuggestionKind(type: any): CompletionItemKind { if (Array.isArray(type)) { - var array = type; + let array = type; type = array.length > 0 ? array[0] : null; } if (!type) { @@ -342,20 +381,20 @@ export class JSONCompletion { case 'object': return '{}'; default: - var content = document.getText().substr(node.start, node.end - node.start); + let content = document.getText().substr(node.start, node.end - node.start); return content; } } private getSnippetForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string { - var result = '"' + key + '"'; + let result = '"' + key + '"'; if (!addValue) { return result; } result += ': '; - var defaultVal = propertySchema.default; + let defaultVal = propertySchema.default; if (typeof defaultVal !== 'undefined') { result = result + this.getSnippetForValue(defaultVal); } else if (propertySchema.enum && propertySchema.enum.length > 0) { @@ -393,4 +432,13 @@ export class JSONCompletion { private getSnippetForSimilarProperty(key: string, templateValue: Parser.ASTNode): string { return '"' + key + '"'; } -} + + private getCurrentWord(document: ITextDocument, offset: number) { + var i = offset - 1; + var text = document.getText(); + while (i >= 0 && ' \t\n\r\v"'.indexOf(text.charAt(i)) === -1) { + i--; + } + return text.substring(i+1, offset); + } +} \ No newline at end of file diff --git a/extensions/json/server/src/jsonContributions.ts b/extensions/json/server/src/jsonContributions.ts new file mode 100644 index 00000000000..bc518f5de84 --- /dev/null +++ b/extensions/json/server/src/jsonContributions.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {JSONLocation} from './jsonLocation'; +import {ISuggestionsCollector} from './jsonCompletion'; + +import {MarkedString} from 'vscode-languageserver'; + +export {ISuggestionsCollector} from './jsonCompletion'; + + +export interface IJSONWorkerContribution { + getInfoContribution(resource: string, location: JSONLocation) : Thenable; + collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable; + collectValueSuggestions(resource: string, location: JSONLocation, propertyKey: string, result: ISuggestionsCollector): Thenable; + collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable; +} \ No newline at end of file diff --git a/extensions/json/server/src/jsonHover.ts b/extensions/json/server/src/jsonHover.ts index 7395c471292..f3c348c74a9 100644 --- a/extensions/json/server/src/jsonHover.ts +++ b/extensions/json/server/src/jsonHover.ts @@ -7,15 +7,18 @@ import Parser = require('./jsonParser'); import SchemaService = require('./jsonSchemaService'); +import {IJSONWorkerContribution} from './jsonContributions'; import {Hover, ITextDocument, TextDocumentPosition, Range, MarkedString, RemoteConsole} from 'vscode-languageserver'; export class JSONHover { private schemaService: SchemaService.IJSONSchemaService; + private contributions: IJSONWorkerContribution[]; - constructor(schemaService: SchemaService.IJSONSchemaService) { + constructor(schemaService: SchemaService.IJSONSchemaService, contributions: IJSONWorkerContribution[] = []) { this.schemaService = schemaService; + this.contributions = contributions; } public doHover(document: ITextDocument, textDocumentPosition: TextDocumentPosition, doc: Parser.JSONDocument): Thenable { @@ -52,15 +55,28 @@ export class JSONHover { } return true; }); - - if (description) { + + function createHover(contents: MarkedString[]) { let range = Range.create(document.positionAt(node.start), document.positionAt(node.end)); let result: Hover = { - contents: [description], + contents: contents, range: range }; return result; } + + let location = node.getNodeLocation(); + for (let i = this.contributions.length - 1; i >= 0; i--) { + let contribution = this.contributions[i]; + let promise = contribution.getInfoContribution(textDocumentPosition.uri, location); + if (promise) { + return promise.then(htmlContent => createHover(htmlContent)); + } + } + + if (description) { + return createHover([description]); + } } return void 0; }); diff --git a/extensions/json/server/src/jsoncontributions/bowerJSONContribution.ts b/extensions/json/server/src/jsoncontributions/bowerJSONContribution.ts new file mode 100644 index 00000000000..66f29913c69 --- /dev/null +++ b/extensions/json/server/src/jsoncontributions/bowerJSONContribution.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {MarkedString, CompletionItemKind} from 'vscode-languageserver'; +import Strings = require('../utils/strings'); +import nls = require('../utils/nls'); +import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions'; +import {IRequestService} from '../jsonSchemaService'; +import {JSONLocation} from '../jsonLocation'; + +export class BowerJSONContribution implements IJSONWorkerContribution { + + private requestService : IRequestService; + + private topRanked = ['twitter','bootstrap','angular-1.1.6','angular-latest','angulerjs','d3','myjquery','jq','abcdef1234567890','jQuery','jquery-1.11.1','jquery', + 'sushi-vanilla-x-data','font-awsome','Font-Awesome','font-awesome','fontawesome','html5-boilerplate','impress.js','homebrew', + 'backbone','moment1','momentjs','moment','linux','animate.css','animate-css','reveal.js','jquery-file-upload','blueimp-file-upload','threejs','express','chosen', + 'normalize-css','normalize.css','semantic','semantic-ui','Semantic-UI','modernizr','underscore','underscore1', + 'material-design-icons','ionic','chartjs','Chart.js','nnnick-chartjs','select2-ng','select2-dist','phantom','skrollr','scrollr','less.js','leancss','parser-lib', + 'hui','bootstrap-languages','async','gulp','jquery-pjax','coffeescript','hammer.js','ace','leaflet','jquery-mobile','sweetalert','typeahead.js','soup','typehead.js', + 'sails','codeigniter2']; + + public constructor(requestService: IRequestService) { + this.requestService = requestService; + } + + private isBowerFile(resource: string): boolean { + return Strings.endsWith(resource, '/bower.json') || Strings.endsWith(resource, '/.bower.json'); + } + + public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable { + if (this.isBowerFile(resource)) { + let defaultValue = { + 'name': '{{name}}', + 'description': '{{description}}', + 'authors': [ '{{author}}' ], + 'version': '{{1.0.0}}', + 'main': '{{pathToMain}}', + 'dependencies': {} + }; + result.add({ kind: CompletionItemKind.Class, label: nls.localize('json.bower.default', 'Default bower.json'), insertText: JSON.stringify(defaultValue, null, '\t'), documentation: '' }); + } + return null; + } + + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable { + if (this.isBowerFile(resource) && (location.matches(['dependencies']) || location.matches(['devDependencies']))) { + if (currentWord.length > 0) { + let queryUrl = 'https://bower.herokuapp.com/packages/search/' + encodeURIComponent(currentWord); + + return this.requestService({ + url : queryUrl + }).then((success) => { + if (success.status === 200) { + try { + let obj = JSON.parse(success.responseText); + if (Array.isArray(obj)) { + let results = <{name:string; description:string;}[]> obj; + for (let i = 0; i < results.length; i++) { + let name = results[i].name; + let description = results[i].description || ''; + let insertText = JSON.stringify(name); + if (addValue) { + insertText += ': "{{*}}"'; + if (!isLast) { + insertText += ','; + } + } + result.add({ kind: CompletionItemKind.Property, label: name, insertText: insertText, documentation: description }); + } + result.setAsIncomplete(); + } + } catch (e) { + // ignore + } + } else { + result.error(nls.localize('json.bower.error.repoaccess', 'Request to the bower repository failed: {0}', success.responseText)); + return 0; + } + }, (error) => { + result.error(nls.localize('json.bower.error.repoaccess', 'Request to the bower repository failed: {0}', error.responseText)); + return 0; + }); + } else { + this.topRanked.forEach((name) => { + let insertText = JSON.stringify(name); + if (addValue) { + insertText += ': "{{*}}"'; + if (!isLast) { + insertText += ','; + } + } + result.add({ kind: CompletionItemKind.Property, label: name, insertText: insertText, documentation: '' }); + }); + result.setAsIncomplete(); + } + } + return null; + } + + public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { + // not implemented. Could be do done calling the bower command. Waiting for web API: https://github.com/bower/registry/issues/26 + return null; + } + + public getInfoContribution(resource: string, location: JSONLocation): Thenable { + if (this.isBowerFile(resource) && (location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) { + let pack = location.getSegments()[location.getSegments().length - 1]; + let htmlContent : MarkedString[] = []; + htmlContent.push(nls.localize('json.bower.package.hover', '{0}', pack)); + + let queryUrl = 'https://bower.herokuapp.com/packages/' + encodeURIComponent(pack); + + return this.requestService({ + url : queryUrl + }).then((success) => { + try { + let obj = JSON.parse(success.responseText); + if (obj && obj.url) { + let url = obj.url; + if (Strings.startsWith(url, 'git://')) { + url = url.substring(6); + } + if (Strings.endsWith(url, '.git')) { + url = url.substring(0, url.length - 4); + } + htmlContent.push(url); + } + } catch (e) { + // ignore + } + return htmlContent; + }, (error) => { + return htmlContent; + }); + } + return null; + } +} \ No newline at end of file diff --git a/extensions/json/server/src/jsoncontributions/globPatternContribution.ts b/extensions/json/server/src/jsoncontributions/globPatternContribution.ts new file mode 100644 index 00000000000..0cf01bc5a9a --- /dev/null +++ b/extensions/json/server/src/jsoncontributions/globPatternContribution.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {MarkedString, CompletionItemKind, CompletionItem} from 'vscode-languageserver'; +import Strings = require('../utils/strings'); +import nls = require('../utils/nls'); +import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions'; +import {IRequestService} from '../jsonSchemaService'; +import {JSONLocation} from '../jsonLocation'; + +let globProperties:CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: nls.localize('fileLabel', "Files by Extension"), insertText: '"**/*.{{extension}}": true', documentation: nls.localize('fileDescription', "Match all files of a specific file extension.")}, + { kind: CompletionItemKind.Value, label: nls.localize('filesLabel', "Files with Multiple Extensions"), insertText: '"**/*.{ext1,ext2,ext3}": true', documentation: nls.localize('filesDescription', "Match all files with any of the file extensions.")}, + { kind: CompletionItemKind.Value, label: nls.localize('derivedLabel', "Files with Siblings by Name"), insertText: '"**/*.{{source-extension}}": { "when": "$(basename).{{target-extension}}" }', documentation: nls.localize('derivedDescription', "Match files that have siblings with the same name but a different extension.")}, + { kind: CompletionItemKind.Value, label: nls.localize('topFolderLabel', "Folder by Name (Top Level)"), insertText: '"{{name}}": true', documentation: nls.localize('topFolderDescription', "Match a top level folder with a specific name.")}, + { kind: CompletionItemKind.Value, label: nls.localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), insertText: '"{folder1,folder2,folder3}": true', documentation: nls.localize('topFoldersDescription', "Match multiple top level folders.")}, + { kind: CompletionItemKind.Value, label: nls.localize('folderLabel', "Folder by Name (Any Location)"), insertText: '"**/{{name}}": true', documentation: nls.localize('folderDescription', "Match a folder with a specific name in any location.")}, +]; + +let globValues:CompletionItem[] = [ + { kind: CompletionItemKind.Value, label: nls.localize('trueLabel', "True"), insertText: 'true', documentation: nls.localize('trueDescription', "Enable the pattern.")}, + { kind: CompletionItemKind.Value, label: nls.localize('falseLabel', "False"), insertText: 'false', documentation: nls.localize('falseDescription', "Disable the pattern.")}, + { kind: CompletionItemKind.Value, label: nls.localize('derivedLabel', "Files with Siblings by Name"), insertText: '{ "when": "$(basename).{{extension}}" }', documentation: nls.localize('siblingsDescription', "Match files that have siblings with the same name but a different extension.")} +]; + +export class GlobPatternContribution implements IJSONWorkerContribution { + + constructor() { + } + + private isSettingsFile(resource: string): boolean { + return Strings.endsWith(resource, '/settings.json'); + } + + public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable { + return null; + } + + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable { + if (this.isSettingsFile(resource) && (location.matches(['files.exclude']) || location.matches(['search.exclude']))) { + + globProperties.forEach((e) => result.add(e)); + } + + return null; + } + + public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { + if (this.isSettingsFile(resource) && (location.matches(['files.exclude']) || location.matches(['search.exclude']))) { + + globValues.forEach((e) => result.add(e)); + } + + return null; + } + + public getInfoContribution(resource: string, location: JSONLocation): Thenable { + return null; + } +} \ No newline at end of file diff --git a/extensions/json/server/src/jsoncontributions/packageJSONContribution.ts b/extensions/json/server/src/jsoncontributions/packageJSONContribution.ts new file mode 100644 index 00000000000..21b411eff3a --- /dev/null +++ b/extensions/json/server/src/jsoncontributions/packageJSONContribution.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {MarkedString, CompletionItemKind, CompletionItem} from 'vscode-languageserver'; +import Strings = require('../utils/strings'); +import nls = require('../utils/nls'); +import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions'; +import {IRequestService} from '../jsonSchemaService'; +import {JSONLocation} from '../jsonLocation'; + +let LIMIT = 40; + +export class PackageJSONContribution implements IJSONWorkerContribution { + + private mostDependedOn = [ 'lodash', 'async', 'underscore', 'request', 'commander', 'express', 'debug', 'chalk', 'colors', 'q', 'coffee-script', + 'mkdirp', 'optimist', 'through2', 'yeoman-generator', 'moment', 'bluebird', 'glob', 'gulp-util', 'minimist', 'cheerio', 'jade', 'redis', 'node-uuid', + 'socket', 'io', 'uglify-js', 'winston', 'through', 'fs-extra', 'handlebars', 'body-parser', 'rimraf', 'mime', 'semver', 'mongodb', 'jquery', + 'grunt', 'connect', 'yosay', 'underscore', 'string', 'xml2js', 'ejs', 'mongoose', 'marked', 'extend', 'mocha', 'superagent', 'js-yaml', 'xtend', + 'shelljs', 'gulp', 'yargs', 'browserify', 'minimatch', 'react', 'less', 'prompt', 'inquirer', 'ws', 'event-stream', 'inherits', 'mysql', 'esprima', + 'jsdom', 'stylus', 'when', 'readable-stream', 'aws-sdk', 'concat-stream', 'chai', 'Thenable', 'wrench']; + + private requestService : IRequestService; + + private isPackageJSONFile(resource: string): boolean { + return Strings.endsWith(resource, '/package.json'); + } + + public constructor(requestService: IRequestService) { + this.requestService = requestService; + } + + public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable { + if (this.isPackageJSONFile(resource)) { + let defaultValue = { + 'name': '{{name}}', + 'description': '{{description}}', + 'author': '{{author}}', + 'version': '{{1.0.0}}', + 'main': '{{pathToMain}}', + 'dependencies': {} + }; + result.add({ kind: CompletionItemKind.Module, label: nls.localize('json.package.default', 'Default package.json'), insertText: JSON.stringify(defaultValue, null, '\t'), documentation: '' }); + } + return null; + } + + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable { + if (this.isPackageJSONFile(resource) && (location.matches(['dependencies']) || location.matches(['devDependencies']) || location.matches(['optionalDependencies']) || location.matches(['peerDependencies']))) { + let queryUrl : string; + if (currentWord.length > 0) { + queryUrl = 'https://skimdb.npmjs.com/registry/_design/app/_view/browseAll?group_level=1&limit=' + LIMIT + '&start_key=%5B%22' + encodeURIComponent(currentWord) + '%22%5D&end_key=%5B%22'+ encodeURIComponent(currentWord + 'z') + '%22,%7B%7D%5D'; + + return this.requestService({ + url : queryUrl + }).then((success) => { + if (success.status === 200) { + try { + let obj = JSON.parse(success.responseText); + if (obj && Array.isArray(obj.rows)) { + let results = <{ key: string[]; }[]> obj.rows; + for (let i = 0; i < results.length; i++) { + let keys = results[i].key; + if (Array.isArray(keys) && keys.length > 0) { + let name = keys[0]; + let insertText = JSON.stringify(name); + if (addValue) { + insertText += ': "{{*}}"'; + if (!isLast) { + insertText += ','; + } + } + result.add({ kind: CompletionItemKind.Property, label: name, insertText: insertText, documentation: '' }); + } + } + if (results.length === LIMIT) { + result.setAsIncomplete(); + } + } + } catch (e) { + // ignore + } + } else { + result.error(nls.localize('json.npm.error.repoaccess', 'Request to the NPM repository failed: {0}', success.responseText)); + return 0; + } + }, (error) => { + result.error(nls.localize('json.npm.error.repoaccess', 'Request to the NPM repository failed: {0}', error.responseText)); + return 0; + }); + } else { + this.mostDependedOn.forEach((name) => { + let insertText = JSON.stringify(name); + if (addValue) { + insertText += ': "{{*}}"'; + if (!isLast) { + insertText += ','; + } + } + result.add({ kind: CompletionItemKind.Property, label: name, insertText: insertText, documentation: '' }); + }); + result.setAsIncomplete(); + } + } + return null; + } + + public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { + if (this.isPackageJSONFile(resource) && (location.matches(['dependencies']) || location.matches(['devDependencies']) || location.matches(['optionalDependencies']) || location.matches(['peerDependencies']))) { + let queryUrl = 'http://registry.npmjs.org/' + encodeURIComponent(currentKey) + '/latest'; + + return this.requestService({ + url : queryUrl + }).then((success) => { + try { + let obj = JSON.parse(success.responseText); + if (obj && obj.version) { + let version = obj.version; + let name = JSON.stringify(version); + result.add({ kind: CompletionItemKind.Class, label: name, insertText: name, documentation: nls.localize('json.npm.latestversion', 'The currently latest version of the package') }); + name = JSON.stringify('^' + version); + result.add({ kind: CompletionItemKind.Class, label: name, insertText: name, documentation: nls.localize('json.npm.majorversion', 'Matches the most recent major version (1.x.x)') }); + name = JSON.stringify('~' + version); + result.add({ kind: CompletionItemKind.Class, label: name, insertText: name, documentation: nls.localize('json.npm.minorversion', 'Matches the most recent minor version (1.2.x)') }); + } + } catch (e) { + // ignore + } + return 0; + }, (error) => { + return 0; + }); + } + return null; + } + + public getInfoContribution(resource: string, location: JSONLocation): Thenable { + if (this.isPackageJSONFile(resource) && (location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']) || location.matches(['optionalDependencies', '*']) || location.matches(['peerDependencies', '*']))) { + let pack = location.getSegments()[location.getSegments().length - 1]; + + let htmlContent : MarkedString[] = []; + htmlContent.push(nls.localize('json.npm.package.hover', '{0}', pack)); + + let queryUrl = 'http://registry.npmjs.org/' + encodeURIComponent(pack) + '/latest'; + + return this.requestService({ + url : queryUrl + }).then((success) => { + try { + let obj = JSON.parse(success.responseText); + if (obj) { + if (obj.description) { + htmlContent.push(obj.description); + } + if (obj.version) { + htmlContent.push(nls.localize('json.npm.version.hover', 'Latest version: {0}', obj.version)); + } + } + } catch (e) { + // ignore + } + return htmlContent; + }, (error) => { + return htmlContent; + }); + } + return null; + } +} \ No newline at end of file diff --git a/extensions/json/server/src/jsoncontributions/projectJSONContribution.ts b/extensions/json/server/src/jsoncontributions/projectJSONContribution.ts new file mode 100644 index 00000000000..29633ff2f0d --- /dev/null +++ b/extensions/json/server/src/jsoncontributions/projectJSONContribution.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {MarkedString, CompletionItemKind, CompletionItem} from 'vscode-languageserver'; +import Strings = require('../utils/strings'); +import nls = require('../utils/nls'); +import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions'; +import {IRequestService} from '../jsonSchemaService'; +import {JSONLocation} from '../jsonLocation'; + +let LIMIT = 40; + +export class ProjectJSONContribution implements IJSONWorkerContribution { + + private requestService : IRequestService; + + public constructor(requestService: IRequestService) { + this.requestService = requestService; + } + + private isProjectJSONFile(resource: string): boolean { + return Strings.endsWith(resource, '/project.json'); + } + + public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable { + if (this.isProjectJSONFile(resource)) { + let defaultValue = { + 'version': '{{1.0.0-*}}', + 'dependencies': {}, + 'frameworks': { + 'dnx451': {}, + 'dnxcore50': {} + } + }; + result.add({ kind: CompletionItemKind.Class, label: nls.localize('json.project.default', 'Default project.json'), insertText: JSON.stringify(defaultValue, null, '\t'), documentation: '' }); + } + return null; + } + + public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable { + if (this.isProjectJSONFile(resource) && (location.matches(['dependencies']) || location.matches(['frameworks', '*', 'dependencies']) || location.matches(['frameworks', '*', 'frameworkAssemblies']))) { + let queryUrl : string; + if (currentWord.length > 0) { + queryUrl = 'https://www.nuget.org/api/v2/Packages?' + + '$filter=Id%20ge%20\'' + + encodeURIComponent(currentWord) + + '\'%20and%20Id%20lt%20\'' + + encodeURIComponent(currentWord + 'z') + + '\'%20and%20IsAbsoluteLatestVersion%20eq%20true' + + '&$select=Id,Version,Description&$format=json&$top=' + LIMIT; + } else { + queryUrl = 'https://www.nuget.org/api/v2/Packages?' + + '$filter=IsAbsoluteLatestVersion%20eq%20true' + + '&$orderby=DownloadCount%20desc&$top=' + LIMIT + + '&$select=Id,Version,DownloadCount,Description&$format=json'; + } + + return this.requestService({ + url : queryUrl + }).then((success) => { + if (success.status === 200) { + try { + let obj = JSON.parse(success.responseText); + if (Array.isArray(obj.d)) { + let results = obj.d; + for (let i = 0; i < results.length; i++) { + let curr = results[i]; + let name = curr.Id; + let version = curr.Version; + if (name) { + let documentation = curr.Description; + let typeLabel = curr.Version; + let insertText = JSON.stringify(name); + if (addValue) { + insertText += ': "{{' + version + '}}"'; + if (!isLast) { + insertText += ','; + } + } + result.add({ kind: CompletionItemKind.Property, label: name, insertText: insertText, detail: typeLabel, documentation: documentation }); + } + } + if (results.length === LIMIT) { + result.setAsIncomplete(); + } + } + } catch (e) { + // ignore + } + } else { + result.error(nls.localize('json.nugget.error.repoaccess', 'Request to the nuget repository failed: {0}', success.responseText)); + return 0; + } + }, (error) => { + result.error(nls.localize('json.nugget.error.repoaccess', 'Request to the nuget repository failed: {0}', error.responseText)); + return 0; + }); + } + return null; + } + + public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable { + if (this.isProjectJSONFile(resource) && (location.matches(['dependencies']) || location.matches(['frameworks', '*', 'dependencies']) || location.matches(['frameworks', '*', 'frameworkAssemblies']))) { + let queryUrl = 'https://www.myget.org/F/aspnetrelease/api/v2/Packages?' + + '$filter=Id%20eq%20\'' + + encodeURIComponent(currentKey) + + '\'&$select=Version,IsAbsoluteLatestVersion&$format=json&$top=' + LIMIT; + + return this.requestService({ + url : queryUrl + }).then((success) => { + try { + let obj = JSON.parse(success.responseText); + if (Array.isArray(obj.d)) { + let results = obj.d; + for (let i = 0; i < results.length; i++) { + let curr = results[i]; + let version = curr.Version; + if (version) { + let name = JSON.stringify(version); + let isLatest = curr.IsAbsoluteLatestVersion === 'true'; + let label = name; + let documentation = ''; + if (isLatest) { + documentation = nls.localize('json.nugget.versiondescription.suggest', 'The currently latest version of the package'); + } + result.add({ kind: CompletionItemKind.Class, label: label, insertText: name, documentation: documentation }); + } + } + if (results.length === LIMIT) { + result.setAsIncomplete(); + } + } + } catch (e) { + // ignore + } + return 0; + }, (error) => { + return 0; + }); + } + return null; + } + + public getInfoContribution(resource: string, location: JSONLocation): Thenable { + if (this.isProjectJSONFile(resource) && (location.matches(['dependencies', '*']) || location.matches(['frameworks', '*', 'dependencies', '*']) || location.matches(['frameworks', '*', 'frameworkAssemblies', '*']))) { + let pack = location.getSegments()[location.getSegments().length - 1]; + + let htmlContent : MarkedString[] = []; + htmlContent.push(nls.localize('json.nugget.package.hover', '{0}', pack)); + + let queryUrl = 'https://www.myget.org/F/aspnetrelease/api/v2/Packages?' + + '$filter=Id%20eq%20\'' + + encodeURIComponent(pack) + + '\'%20and%20IsAbsoluteLatestVersion%20eq%20true' + + '&$select=Version,Description&$format=json&$top=5'; + + return this.requestService({ + url : queryUrl + }).then((success) => { + let content = success.responseText; + if (content) { + try { + let obj = JSON.parse(content); + if (obj.d && obj.d[0]) { + let res = obj.d[0]; + if (res.Description) { + htmlContent.push(res.Description); + } + if (res.Version) { + htmlContent.push(nls.localize('json.nugget.version.hover', 'Latest version: {0}', res.Version)); + } + } + } catch (e) { + // ignore + } + } + return htmlContent; + }, (error) => { + return htmlContent; + }); + } + return null; + } +} \ No newline at end of file diff --git a/extensions/json/server/src/server.ts b/extensions/json/server/src/server.ts index 0994ad1dd1d..eca86617647 100644 --- a/extensions/json/server/src/server.ts +++ b/extensions/json/server/src/server.ts @@ -25,6 +25,10 @@ import {JSONHover} from './jsonHover'; import {JSONDocumentSymbols} from './jsonDocumentSymbols'; import {format as formatJSON} from './jsonFormatter'; import {schemaContributions} from './configuration'; +import {BowerJSONContribution} from './jsoncontributions/bowerJSONContribution'; +import {PackageJSONContribution} from './jsoncontributions/packageJSONContribution'; +import {ProjectJSONContribution} from './jsoncontributions/projectJSONContribution'; +import {GlobPatternContribution} from './jsoncontributions/globPatternContribution'; namespace TelemetryNotification { export const type: NotificationType<{ key: string, data: any }> = { get method() { return 'telemetry'; } }; @@ -106,11 +110,18 @@ let request = (options: IXHROptions): Thenable => { return xhr(options); } +let contributions = [ + new ProjectJSONContribution(request), + new PackageJSONContribution(request), + new BowerJSONContribution(request), + new GlobPatternContribution() +]; + let jsonSchemaService = new JSONSchemaService(request, workspaceContext, telemetry); jsonSchemaService.setSchemaContributions(schemaContributions); -let jsonCompletion = new JSONCompletion(jsonSchemaService); -let jsonHover = new JSONHover(jsonSchemaService); +let jsonCompletion = new JSONCompletion(jsonSchemaService, contributions); +let jsonHover = new JSONHover(jsonSchemaService, contributions); let jsonDocumentSymbols = new JSONDocumentSymbols(); // The content of a text document has changed. This event is emitted diff --git a/extensions/json/server/src/utils/httpRequest.ts b/extensions/json/server/src/utils/httpRequest.ts index 4940ed2b133..5b0ca9bc11e 100644 --- a/extensions/json/server/src/utils/httpRequest.ts +++ b/extensions/json/server/src/utils/httpRequest.ts @@ -22,7 +22,7 @@ export interface IXHROptions { agent?: any; strictSSL?: boolean; responseType?: string; - followRedirects: number; + followRedirects?: number; } export interface IXHRResponse { @@ -47,6 +47,9 @@ export function xhr(options: IXHROptions): Promise { const agent = getProxyAgent(options.url, { proxyUrl, strictSSL }); options = assign({}, options); options = assign(options, { agent, strictSSL }); + if (typeof options.followRedirects !== 'number') { + options.followRedirects = 5; + } return request(options).then(result => new Promise((c, e) => { let res = result.res;