Files
vscode/extensions/javascript/src/features/jsonContributions.ts
2017-03-21 22:48:37 +01:00

164 lines
6.3 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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 { Location, getLocation, createScanner, SyntaxKind, ScanError } from 'jsonc-parser';
import { basename } from 'path';
import { BowerJSONContribution } from './bowerJSONContribution';
import { PackageJSONContribution } from './packageJSONContribution';
import { XHRRequest } from 'request-light';
import {
CompletionItem, CompletionItemProvider, CompletionList, TextDocument, Position, Hover, HoverProvider,
CancellationToken, Range, MarkedString, DocumentSelector, languages, Disposable
} from 'vscode';
export interface ISuggestionsCollector {
add(suggestion: CompletionItem): void;
error(message: string): void;
log(message: string): void;
setAsIncomplete(): void;
}
export interface IJSONContribution {
getDocumentSelector(): DocumentSelector;
getInfoContribution(fileName: string, location: Location): Thenable<MarkedString[]>;
collectPropertySuggestions(fileName: string, location: Location, currentWord: string, addValue: boolean, isLast: boolean, result: ISuggestionsCollector): Thenable<any>;
collectValueSuggestions(fileName: string, location: Location, result: ISuggestionsCollector): Thenable<any>;
collectDefaultSuggestions(fileName: string, result: ISuggestionsCollector): Thenable<any>;
resolveSuggestion?(item: CompletionItem): Thenable<CompletionItem>;
}
export function addJSONProviders(xhr: XHRRequest): Disposable {
let contributions = [new PackageJSONContribution(xhr), new BowerJSONContribution(xhr)];
let subscriptions: Disposable[] = [];
contributions.forEach(contribution => {
let selector = contribution.getDocumentSelector();
subscriptions.push(languages.registerCompletionItemProvider(selector, new JSONCompletionItemProvider(contribution), '"', ':'));
subscriptions.push(languages.registerHoverProvider(selector, new JSONHoverProvider(contribution)));
});
return Disposable.from(...subscriptions);
}
export class JSONHoverProvider implements HoverProvider {
constructor(private jsonContribution: IJSONContribution) {
}
public provideHover(document: TextDocument, position: Position, token: CancellationToken): Thenable<Hover> {
let fileName = basename(document.fileName);
let offset = document.offsetAt(position);
let location = getLocation(document.getText(), offset);
let node = location.previousNode;
if (node && node.offset <= offset && offset <= node.offset + node.length) {
let promise = this.jsonContribution.getInfoContribution(fileName, location);
if (promise) {
return promise.then(htmlContent => {
let range = new Range(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
let result: Hover = {
contents: htmlContent,
range: range
};
return result;
});
}
}
return null;
}
}
export class JSONCompletionItemProvider implements CompletionItemProvider {
constructor(private jsonContribution: IJSONContribution) {
}
public resolveCompletionItem(item: CompletionItem, token: CancellationToken): Thenable<CompletionItem> {
if (this.jsonContribution.resolveSuggestion) {
let resolver = this.jsonContribution.resolveSuggestion(item);
if (resolver) {
return resolver;
}
}
return Promise.resolve(item);
}
public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Thenable<CompletionList> {
let fileName = basename(document.fileName);
let currentWord = this.getCurrentWord(document, position);
let overwriteRange: Range;
let items: CompletionItem[] = [];
let isIncomplete = false;
let offset = document.offsetAt(position);
let location = getLocation(document.getText(), offset);
let node = location.previousNode;
if (node && node.offset <= offset && offset <= node.offset + node.length && (node.type === 'property' || node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
overwriteRange = new Range(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
} else {
overwriteRange = new Range(document.positionAt(offset - currentWord.length), position);
}
let proposed: { [key: string]: boolean } = {};
let collector: ISuggestionsCollector = {
add: (suggestion: CompletionItem) => {
if (!proposed[suggestion.label]) {
proposed[suggestion.label] = true;
suggestion.range = overwriteRange;
items.push(suggestion);
}
},
setAsIncomplete: () => isIncomplete = true,
error: (message: string) => console.error(message),
log: (message: string) => console.log(message)
};
let collectPromise: Thenable<any> = null;
if (location.isAtPropertyKey) {
let addValue = !location.previousNode || !location.previousNode.columnOffset;
let isLast = this.isLast(document, position);
collectPromise = this.jsonContribution.collectPropertySuggestions(fileName, location, currentWord, addValue, isLast, collector);
} else {
if (location.path.length === 0) {
collectPromise = this.jsonContribution.collectDefaultSuggestions(fileName, collector);
} else {
collectPromise = this.jsonContribution.collectValueSuggestions(fileName, location, collector);
}
}
if (collectPromise) {
return collectPromise.then(() => {
if (items.length > 0) {
return new CompletionList(items, isIncomplete);
}
return null;
});
}
return null;
}
private getCurrentWord(document: TextDocument, position: Position) {
var i = position.character - 1;
var text = document.lineAt(position.line).text;
while (i >= 0 && ' \t\n\r\v":{[,'.indexOf(text.charAt(i)) === -1) {
i--;
}
return text.substring(i + 1, position.character);
}
private isLast(document: TextDocument, position: Position): boolean {
let scanner = createScanner(document.getText(), true);
scanner.setPosition(document.offsetAt(position));
let nextToken = scanner.scan();
if (nextToken === SyntaxKind.StringLiteral && scanner.getTokenError() === ScanError.UnexpectedEndOfString) {
nextToken = scanner.scan();
}
return nextToken === SyntaxKind.CloseBraceToken || nextToken === SyntaxKind.EOF;
}
}