Fixes #3204: [json] project.json intellisense broken due to server change

This commit is contained in:
Martin Aeschlimann
2016-03-03 16:09:16 +01:00
parent c29826b209
commit 63c9d46cde
6 changed files with 248 additions and 147 deletions

View File

@@ -4,19 +4,34 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {MarkedString, CompletionItemKind} from 'vscode-languageserver';
import {MarkedString, CompletionItemKind, CompletionItem} from 'vscode-languageserver';
import Strings = require('../utils/strings');
import nls = require('../utils/nls');
import {IXHRResponse, getErrorStatusDescription} from '../utils/httpRequest';
import {IJSONWorkerContribution, ISuggestionsCollector} from '../jsonContributions';
import {IRequestService} from '../jsonSchemaService';
import {JSONLocation} from '../jsonLocation';
let LIMIT = 40;
const FEED_INDEX_URL = 'https://api.nuget.org/v3/index.json';
const LIMIT = 30;
const RESOLVE_ID = 'ProjectJSONContribution-';
const CACHE_EXPIRY = 1000 * 60 * 5; // 5 minutes
interface NugetServices {
'SearchQueryService'?: string;
'SearchAutocompleteService'?: string;
'PackageBaseAddress/3.0.0'?: string;
[key: string]: string;
}
export class ProjectJSONContribution implements IJSONWorkerContribution {
private requestService : IRequestService;
private cachedProjects: { [id: string]: { version: string, description: string, time: number }} = {};
private cacheSize: number = 0;
private nugetIndexPromise: Thenable<NugetServices>;
public constructor(requestService: IRequestService) {
this.requestService = requestService;
}
@@ -24,6 +39,67 @@ export class ProjectJSONContribution implements IJSONWorkerContribution {
private isProjectJSONFile(resource: string): boolean {
return Strings.endsWith(resource, '/project.json');
}
private completeWithCache(id: string, item: CompletionItem) : boolean {
let entry = this.cachedProjects[id];
if (entry) {
if (new Date().getTime() - entry.time > CACHE_EXPIRY) {
delete this.cachedProjects[id];
this.cacheSize--;
return false;
}
item.detail = entry.version;
item.documentation = entry.description;
item.insertText = item.insertText.replace(/\{\{\}\}/, '{{' + entry.version + '}}');
return true;
}
return false;
}
private addCached(id: string, version: string, description: string) {
this.cachedProjects[id] = { version, description, time: new Date().getTime()};
this.cacheSize++;
if (this.cacheSize > 50) {
let currentTime = new Date().getTime() ;
for (var id in this.cachedProjects) {
let entry = this.cachedProjects[id];
if (currentTime - entry.time > CACHE_EXPIRY) {
delete this.cachedProjects[id];
this.cacheSize--;
}
}
}
}
private getNugetIndex() : Thenable<NugetServices> {
if (!this.nugetIndexPromise) {
this.nugetIndexPromise = this.makeJSONRequest<any>(FEED_INDEX_URL).then(indexContent => {
let services : NugetServices = {};
if (indexContent && Array.isArray(indexContent.resources)) {
let resources = <any[]> indexContent.resources;
for (let i = resources.length - 1; i >= 0; i--) {
let type = resources[i]['@type'];
let id = resources[i]['@id'];
if (type && id) {
services[type] = id;
}
}
}
return services;
});
}
return this.nugetIndexPromise;
}
private getNugetService(serviceType: string) : Thenable<string> {
return this.getNugetIndex().then(services => {
let serviceURL = services[serviceType];
if (!serviceURL) {
return Promise.reject<string>(nls.localize('json.nugget.error.missingservice', 'NuGet index document is missing service {0}', serviceType));
}
return serviceURL;
});
}
public collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable<any> {
if (this.isProjectJSONFile(resource)) {
@@ -40,106 +116,88 @@ export class ProjectJSONContribution implements IJSONWorkerContribution {
return null;
}
public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable<any> {
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 = <any[]> 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;
private makeJSONRequest<T>(url: string) : Thenable<T> {
return this.requestService({
url : url
}).then(success => {
if (success.status === 200) {
try {
return <T> JSON.parse(success.responseText);
} catch (e) {
return Promise.reject<T>(nls.localize('json.nugget.error.invalidformat', '{0} is not a valid JSON document', url));
}
}, (error) => {
result.error(nls.localize('json.nugget.error.repoaccess', 'Request to the nuget repository failed: {0}', error.responseText));
return 0;
});
}
return null;
}
return Promise.reject<T>(nls.localize('json.nugget.error.indexaccess', 'Request to {0} failed: {1}', url, success.responseText));
}, (error: IXHRResponse) => {
return Promise.reject<T>(nls.localize('json.nugget.error.access', 'Request to {0} failed: {1}', url, getErrorStatusDescription(error.status)));
});
}
public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable<any> {
public collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable<any> {
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 = <any[]> obj.d;
return this.getNugetService('SearchAutocompleteService').then(service => {
let queryUrl : string;
if (currentWord.length > 0) {
queryUrl = service + '?q=' + encodeURIComponent(currentWord) +'&take=' + LIMIT;
} else {
queryUrl = service + '?take=' + LIMIT;
}
return this.makeJSONRequest<any>(queryUrl).then(resultObj => {
if (Array.isArray(resultObj.data)) {
let results = <any[]> resultObj.data;
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');
let name = results[i];
let insertText = JSON.stringify(name);
if (addValue) {
insertText += ': "{{}}"';
if (!isLast) {
insertText += ',';
}
result.add({ kind: CompletionItemKind.Class, label: label, insertText: name, documentation: documentation });
}
let item : CompletionItem = { kind: CompletionItemKind.Property, label: name, insertText: insertText };
if (!this.completeWithCache(name, item)) {
item.data = RESOLVE_ID + name;
}
result.add(item);
}
if (results.length === LIMIT) {
result.setAsIncomplete();
}
}
} catch (e) {
// ignore
}
return 0;
}, (error) => {
return 0;
}, error => {
result.error(error);
});
}, error => {
result.error(error);
});
};
return null;
}
public collectValueSuggestions(resource: string, location: JSONLocation, currentKey: string, result: ISuggestionsCollector): Thenable<any> {
if (this.isProjectJSONFile(resource) && (location.matches(['dependencies']) || location.matches(['frameworks', '*', 'dependencies']) || location.matches(['frameworks', '*', 'frameworkAssemblies']))) {
return this.getNugetService('PackageBaseAddress/3.0.0').then(service => {
let queryUrl = service + currentKey + '/index.json';
return this.makeJSONRequest<any>(queryUrl).then(obj => {
if (Array.isArray(obj.versions)) {
let results = <any[]> obj.versions;
for (let i = 0; i < results.length; i++) {
let curr = results[i];
let name = JSON.stringify(curr);
let label = name;
let documentation = '';
result.add({ kind: CompletionItemKind.Class, label: label, insertText: name, documentation: documentation });
}
if (results.length === LIMIT) {
result.setAsIncomplete();
}
}
}, error => {
result.error(error);
});
}, error => {
result.error(error);
});
}
return null;
@@ -149,40 +207,63 @@ export class ProjectJSONContribution implements IJSONWorkerContribution {
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));
return this.getNugetService('SearchQueryService').then(service => {
let queryUrl = service + '?q=' + encodeURIComponent(pack) +'&take=' + 5;
return this.makeJSONRequest<any>(queryUrl).then(resultObj => {
let htmlContent : MarkedString[] = [];
htmlContent.push(nls.localize('json.nugget.package.hover', '{0}', pack));
if (Array.isArray(resultObj.data)) {
let results = <any[]> resultObj.data;
for (let i = 0; i < results.length; i++) {
let res = results[i];
this.addCached(res.id, res.version, res.description);
if (res.id === pack) {
if (res.description) {
htmlContent.push(res.description);
}
if (res.version) {
htmlContent.push(nls.localize('json.nugget.version.hover', 'Latest version: {0}', res.version));
}
break;
}
}
} catch (e) {
// ignore
}
}
return htmlContent;
return htmlContent;
}, (error) => {
return null;
});
}, (error) => {
return htmlContent;
return null;
});
}
return null;
}
public resolveSuggestion(item: CompletionItem) : Thenable<CompletionItem> {
if (item.data && Strings.startsWith(item.data, RESOLVE_ID)) {
let pack = item.data.substring(RESOLVE_ID.length);
if (this.completeWithCache(pack, item)) {
return Promise.resolve(item);
}
return this.getNugetService('SearchQueryService').then(service => {
let queryUrl = service + '?q=' + encodeURIComponent(pack) +'&take=' + 10;
return this.makeJSONRequest<any>(queryUrl).then(resultObj => {
let itemResolved = false;
if (Array.isArray(resultObj.data)) {
let results = <any[]> resultObj.data;
for (let i = 0; i < results.length; i++) {
let curr = results[i];
this.addCached(curr.id, curr.version, curr.description);
if (curr.id === pack) {
this.completeWithCache(pack, item);
itemResolved = true;
}
}
}
return itemResolved ? item : null;
});
});
};
return null;
}
}