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

@ -11,11 +11,12 @@ import JsonSchema = require('./json-toolbox/jsonSchema');
import nls = require('./utils/nls');
import {IJSONWorkerContribution} from './jsonContributions';
import {CompletionItem, CompletionItemKind, CompletionList, ITextDocument, TextDocumentPosition, Range, TextEdit} from 'vscode-languageserver';
import {CompletionItem, CompletionItemKind, CompletionList, ITextDocument, TextDocumentPosition, Range, TextEdit, RemoteConsole} from 'vscode-languageserver';
export interface ISuggestionsCollector {
add(suggestion: CompletionItem): void;
error(message:string): void;
log(message:string): void;
setAsIncomplete(): void;
}
@ -23,10 +24,24 @@ export class JSONCompletion {
private schemaService: SchemaService.IJSONSchemaService;
private contributions: IJSONWorkerContribution[];
private console: RemoteConsole;
constructor(schemaService: SchemaService.IJSONSchemaService, contributions: IJSONWorkerContribution[] = []) {
constructor(schemaService: SchemaService.IJSONSchemaService, console: RemoteConsole, contributions: IJSONWorkerContribution[] = []) {
this.schemaService = schemaService;
this.contributions = contributions;
this.console = console;
}
public doResolve(item: CompletionItem) : Thenable<CompletionItem> {
for (let i = this.contributions.length - 1; i >= 0; i--) {
if (this.contributions[i].resolveSuggestion) {
let resolver = this.contributions[i].resolveSuggestion(item);
if (resolver) {
return resolver;
}
}
}
return Promise.resolve(item);
}
public doSuggest(document: ITextDocument, textDocumentPosition: TextDocumentPosition, doc: Parser.JSONDocument): Thenable<CompletionList> {
@ -63,7 +78,10 @@ export class JSONCompletion {
result.isIncomplete = true;
},
error: (message: string) => {
console.log(message);
this.console.error(message);
},
log: (message: string) => {
this.console.log(message);
}
};

View file

@ -7,7 +7,7 @@
import {JSONLocation} from './jsonLocation';
import {ISuggestionsCollector} from './jsonCompletion';
import {MarkedString} from 'vscode-languageserver';
import {MarkedString, CompletionItem} from 'vscode-languageserver';
export {ISuggestionsCollector} from './jsonCompletion';
@ -17,4 +17,5 @@ export interface IJSONWorkerContribution {
collectPropertySuggestions(resource: string, location: JSONLocation, currentWord: string, addValue: boolean, isLast:boolean, result: ISuggestionsCollector) : Thenable<any>;
collectValueSuggestions(resource: string, location: JSONLocation, propertyKey: string, result: ISuggestionsCollector): Thenable<any>;
collectDefaultSuggestions(resource: string, result: ISuggestionsCollector): Thenable<any>;
resolveSuggestion?(item: CompletionItem): Thenable<CompletionItem>;
}

View file

@ -39,6 +39,24 @@ export class JSONHover {
if (!node) {
return Promise.resolve(void 0);
}
var createHover = (contents: MarkedString[]) => {
let range = Range.create(document.positionAt(node.start), document.positionAt(node.end));
let result: Hover = {
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));
}
}
return this.schemaService.getSchemaForResource(textDocumentPosition.uri, doc).then((schema) => {
if (schema) {
@ -46,33 +64,12 @@ export class JSONHover {
doc.validate(schema.schema, matchingSchemas, node.start);
let description: string = null;
let contributonId: string = null;
matchingSchemas.every((s) => {
if (s.node === node && !s.inverted && s.schema) {
description = description || s.schema.description;
contributonId = contributonId || s.schema.id;
}
return true;
});
var createHover = (contents: MarkedString[]) => {
let range = Range.create(document.positionAt(node.start), document.positionAt(node.end));
let result: Hover = {
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]);
}

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;
}
}

View file

@ -9,7 +9,7 @@ import {
createConnection, IConnection,
TextDocuments, ITextDocument, Diagnostic, DiagnosticSeverity,
InitializeParams, InitializeResult, TextDocumentIdentifier, TextDocumentPosition, CompletionList,
Hover, SymbolInformation, DocumentFormattingParams,
CompletionItem, Hover, SymbolInformation, DocumentFormattingParams,
DocumentRangeFormattingParams, NotificationType, RequestType
} from 'vscode-languageserver';
@ -63,7 +63,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
capabilities: {
// Tell the client that the server works in FULL text document sync mode
textDocumentSync: documents.syncKind,
completionProvider: { resolveProvider: false },
completionProvider: { resolveProvider: true },
hoverProvider: true,
documentSymbolProvider: true,
documentRangeFormattingProvider: true,
@ -121,7 +121,7 @@ let contributions = [
let jsonSchemaService = new JSONSchemaService(request, workspaceContext, telemetry);
jsonSchemaService.setSchemaContributions(schemaContributions);
let jsonCompletion = new JSONCompletion(jsonSchemaService, contributions);
let jsonCompletion = new JSONCompletion(jsonSchemaService, connection.console, contributions);
let jsonHover = new JSONHover(jsonSchemaService, contributions);
let jsonDocumentSymbols = new JSONDocumentSymbols();
@ -269,6 +269,10 @@ connection.onCompletion((textDocumentPosition: TextDocumentPosition): Thenable<C
return jsonCompletion.doSuggest(document, textDocumentPosition, jsonDocument);
});
connection.onCompletionResolve((item: CompletionItem) : Thenable<CompletionItem> => {
return jsonCompletion.doResolve(item);
});
connection.onHover((textDocumentPosition: TextDocumentPosition): Thenable<Hover> => {
let document = documents.get(textDocumentPosition.uri);
let jsonDocument = getJSONDocument(document);

View file

@ -36,7 +36,7 @@ suite('JSON Completion', () => {
var idx = stringAfter ? value.indexOf(stringAfter) : 0;
var schemaService = new SchemaService.JSONSchemaService(requestService);
var completionProvider = new JSONCompletion(schemaService);
var completionProvider = new JSONCompletion(schemaService, console);
if (schema) {
var id = "http://myschemastore/test1";
schemaService.registerExternalSchema(id, ["*.json"], schema);