[html] bring back provider configuration settings

This commit is contained in:
Martin Aeschlimann 2016-09-14 16:27:53 +02:00
parent cf1596c6f0
commit 6d788bff70
6 changed files with 66 additions and 135 deletions

View file

@ -9,7 +9,7 @@ import {
TextDocuments, TextDocument, InitializeParams, InitializeResult
} from 'vscode-languageserver';
import {HTMLDocument, LanguageSettings, getLanguageService} from './service/htmlLanguageService';
import {HTMLDocument, getLanguageService, CompletionConfiguration, HTMLFormatConfiguration} from './service/htmlLanguageService';
import * as nls from 'vscode-nls';
nls.config(process.env['VSCODE_NLS_CONFIG']);
@ -51,66 +51,19 @@ interface Settings {
html: LanguageSettings;
}
interface LanguageSettings {
suggest: CompletionConfiguration;
format: HTMLFormatConfiguration;
}
let languageSettings: LanguageSettings;
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
var settings = <Settings>change.settings;
languageSettings = settings.html;
updateConfiguration();
});
function updateConfiguration() {
languageService.configure(languageSettings);
// Revalidate any open text documents
documents.all().forEach(triggerValidation);
}
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent((change) => {
triggerValidation(change.document);
});
// a document has closed: clear all diagnostics
documents.onDidClose(event => {
cleanPendingValidation(event.document);
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
});
let pendingValidationRequests : {[uri:string]:NodeJS.Timer} = {};
const validationDelayMs = 200;
function cleanPendingValidation(textDocument: TextDocument): void {
let request = pendingValidationRequests[textDocument.uri];
if (request) {
clearTimeout(request);
delete pendingValidationRequests[textDocument.uri];
}
}
function triggerValidation(textDocument: TextDocument): void {
cleanPendingValidation(textDocument);
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
delete pendingValidationRequests[textDocument.uri];
validateTextDocument(textDocument);
}, validationDelayMs);
}
function validateTextDocument(textDocument: TextDocument): void {
if (textDocument.getText().length === 0) {
// ignore empty documents
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
return;
}
let htmlDocument = getHTMLDocument(textDocument);
let diagnostics = languageService.doValidation(textDocument, htmlDocument);
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
function getHTMLDocument(document: TextDocument): HTMLDocument {
return languageService.parseHTMLDocument(document);
}
@ -118,7 +71,8 @@ function getHTMLDocument(document: TextDocument): HTMLDocument {
connection.onCompletion(textDocumentPosition => {
let document = documents.get(textDocumentPosition.textDocument.uri);
let htmlDocument = getHTMLDocument(document);
return languageService.doComplete(document, textDocumentPosition.position, htmlDocument);
let options = languageSettings && languageSettings.suggest;
return languageService.doComplete(document, textDocumentPosition.position, htmlDocument, options);
});
connection.onDocumentHighlight(documentHighlightParams => {

View file

@ -41,27 +41,22 @@ export interface HTMLFormatConfiguration {
extraLiners: string;
}
export interface LanguageSettings {
validate: boolean;
format: HTMLFormatConfiguration;
export interface CompletionConfiguration {
[provider:string]:boolean;
}
export declare type HTMLDocument = {};
export interface LanguageService {
configure(settings: LanguageSettings): void;
parseHTMLDocument(document: TextDocument): HTMLDocument;
doValidation(document: TextDocument, htmlDocument: HTMLDocument): Diagnostic[];
findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[];
doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): CompletionList;
doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, options?: CompletionConfiguration): CompletionList;
format(document: TextDocument, range: Range, options: HTMLFormatConfiguration): TextEdit[];
provideLinks(document: TextDocument, workspacePath:string): DocumentLink[];
}
export function getLanguageService() : LanguageService {
return {
doValidation: (document, htmlDocument) => { return []; },
configure: (settings) => {},
parseHTMLDocument: (document) => parse(document.getText()),
doComplete,
format,

View file

@ -50,6 +50,8 @@ export function isSameTag(t1: string, t2: string) : boolean {
}
export interface IHTMLTagProvider {
getId(): string;
isApplicable(languageId: string);
collectTags(collector: (tag: string, label: string) => void): void;
collectAttributes(tag: string, collector: (attribute: string, type: string) => void): void;
collectValues(tag: string, attribute: string, collector: (value: string) => void): void;
@ -481,6 +483,8 @@ export function getHTML5TagProvider(): IHTMLTagProvider {
};
return {
getId: () => 'html5',
isApplicable: () => true,
collectTags: (collector: (tag: string, label: string) => void) => collectTagsDefault(collector, HTML_TAGS),
collectAttributes: (tag: string, collector: (attribute: string, type: string) => void) => {
collectAttributesDefault(tag, collector, HTML_TAGS, globalAttributes);
@ -508,6 +512,8 @@ export function getAngularTagProvider(): IHTMLTagProvider {
];
return {
getId: () => 'angular1',
isApplicable: (languageId) => languageId === 'html',
collectTags: (collector: (tag: string) => void) => {
// no extra tags
},
@ -555,6 +561,8 @@ export function getIonicTagProvider(): IHTMLTagProvider {
};
return {
getId: () => 'ionic',
isApplicable: (languageId) => languageId === 'html',
collectTags: (collector: (tag: string, label: string) => void) => collectTagsDefault(collector, IONIC_TAGS),
collectAttributes: (tag: string, collector: (attribute: string, type: string) => void) => {
collectAttributesDefault(tag, collector, IONIC_TAGS, globalAttributes);

View file

@ -7,21 +7,22 @@
import { TextDocument, Position, CompletionList, CompletionItemKind, Range } from 'vscode-languageserver-types';
import { HTMLDocument } from '../parser/htmlParser';
import { TokenType, createScanner, ScannerState } from '../parser/htmlScanner';
import { IHTMLTagProvider, getHTML5TagProvider, getAngularTagProvider, getIonicTagProvider } from '../parser/htmlTags';
import { startsWith } from '../utils/strings';
import { getHTML5TagProvider, getAngularTagProvider, getIonicTagProvider } from '../parser/htmlTags';
import { CompletionConfiguration } from '../htmlLanguageService';
let tagProviders: IHTMLTagProvider[] = [];
tagProviders.push(getHTML5TagProvider());
tagProviders.push(getAngularTagProvider());
tagProviders.push(getIonicTagProvider());
let allTagProviders = [
getHTML5TagProvider(),
getAngularTagProvider(),
getIonicTagProvider()
];
export function doComplete(document: TextDocument, position: Position, doc: HTMLDocument): CompletionList {
export function doComplete(document: TextDocument, position: Position, doc: HTMLDocument, settings?: CompletionConfiguration): CompletionList {
let result: CompletionList = {
isIncomplete: false,
items: []
};
let tagProviders = allTagProviders.filter(p => p.isApplicable(document.languageId) && (!settings || !!settings[p.getId()]));
let offset = document.offsetAt(position);
let node = doc.findNodeBefore(offset);
@ -56,8 +57,8 @@ export function doComplete(document: TextDocument, position: Position, doc: HTML
function collectCloseTagSuggestions(afterOpenBracket: number, matchingOnly: boolean) : CompletionList {
let range = getReplaceRange(afterOpenBracket);
let contentAfter = document.getText().substr(offset, 1);
let closeTag = isWhiteSpace(contentAfter) || startsWith(contentAfter, '<') ? '>' : '';
let contentAfter = document.getText().substr(offset);
let closeTag = contentAfter.match(/^\s*>/) ? '' : '>';
let curr = node;
while (curr) {
let tag = curr.tag;

View file

@ -25,7 +25,6 @@ export function findDocumentHighlights(document: TextDocument, position: Positio
result.push({ kind: DocumentHighlightKind.Read, range: endTagRange });
}
}
console.log('foo' + result.length);
return result;
}

View file

@ -17,6 +17,7 @@ export interface ItemDescription {
insertText?: string;
overwriteBefore?: number;
resultText?: string;
notAvailable?: boolean;
}
function asPromise<T>(result: T): Promise<T> {
@ -27,6 +28,11 @@ export let assertCompletion = function (completions: CompletionList, expected: I
let matches = completions.items.filter(completion => {
return completion.label === expected.label;
});
if (expected.notAvailable) {
assert.equal(matches.length, 0, expected.label + " should not existing is results");
return;
}
assert.equal(matches.length, 1, expected.label + " should only existing once: Actual: " + completions.items.map(c => c.label).join(', '));
if (expected.documentation) {
assert.equal(matches[0].documentation, expected.documentation);
@ -52,7 +58,7 @@ export let assertCompletion = function (completions: CompletionList, expected: I
}
};
let testCompletionFor = function (value: string, expected: { count?: number, items?: ItemDescription[] }): Thenable<void> {
let testCompletionFor = function (value: string, expected: { count?: number, items?: ItemDescription[] }, settings? : htmlLanguageService.CompletionConfiguration): Thenable<void> {
let offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
@ -61,7 +67,7 @@ let testCompletionFor = function (value: string, expected: { count?: number, ite
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
let position = document.positionAt(offset);
let htmlDoc = ls.parseHTMLDocument(document);
return asPromise(ls.doComplete(document, position, htmlDoc)).then(list => {
return asPromise(ls.doComplete(document, position, htmlDoc, settings)).then(list => {
try {
if (expected.count) {
assert.equal(list.items, expected.count);
@ -87,7 +93,7 @@ function run(tests: Thenable<void>[], testDone) {
suite('HTML Completion', () => {
test('Intellisense', function (testDone): any {
test('Complete', function (testDone): any {
run([
testCompletionFor('<|', {
items: [
@ -271,7 +277,7 @@ suite('HTML Completion', () => {
suite('Handlevar Completion', (testDone) => {
run([
testCompletionFor('<script id="entry-template" type="text/x-handlebars-template"> | </script>' , {
items: [
{ label: 'div', resultText: '<script id="entry-template" type="text/x-handlebars-template"> <div></div> </script>' },
@ -280,7 +286,7 @@ suite('HTML Completion', () => {
], testDone);
});
test('Intellisense aria', function (testDone): any {
test('Complete aria', function (testDone): any {
let expectedAriaAttributes = [
{ label: 'aria-activedescendant' },
{ label: 'aria-atomic' },
@ -338,7 +344,7 @@ suite('HTML Completion', () => {
], testDone);
});
test('Intellisense Angular', function (testDone): any {
test('Complete Angular', function (testDone): any {
run([
testCompletionFor('<body |> </body >', {
items: [
@ -361,70 +367,38 @@ suite('HTML Completion', () => {
], testDone);
});
test('Intellisense Ionic', function (testDone): any {
test('Complete Ionic', function (testDone): any {
run([
// Try some Ionic tags
testCompletionFor('<|', {
items: [
{ label: 'ion-checkbox', resultText: '<ion-checkbox' },
{ label: 'ion-content', resultText: '<ion-content' },
{ label: 'ion-nav-title', resultText: '<ion-nav-title' },
]
}),
testCompletionFor('<ion-re|', {
items: [
{ label: 'ion-refresher', resultText: '<ion-refresher' },
{ label: 'ion-reorder-button', resultText: '<ion-reorder-button' },
]
}),
// Try some global attributes (1 with value suggestions, 1 without value suggestions, 1 void)
testCompletionFor('<ion-checkbox |', {
items: [
{ label: 'force-refresh-images', resultText: '<ion-checkbox force-refresh-images="{{}}"' },
{ label: 'collection-repeat', resultText: '<ion-checkbox collection-repeat="{{}}"' },
{ label: 'menu-close', resultText: '<ion-checkbox menu-close' },
]
}),
// Try some tag-specific attributes (1 with value suggestions, 1 void)
testCompletionFor('<ion-footer-bar |', {
items: [
{ label: 'align-title', resultText: '<ion-footer-bar align-title="{{}}"' },
{ label: 'keyboard-attach', resultText: '<ion-footer-bar keyboard-attach' },
]
}),
// Try the extended attributes of an existing HTML 5 tag
testCompletionFor('<a |', {
items: [
{ label: 'nav-direction', resultText: '<a nav-direction="{{}}"' },
{ label: 'nav-transition', resultText: '<a nav-transition="{{}}"' },
{ label: 'href', resultText: '<a href="{{}}"' },
{ label: 'hreflang', resultText: '<a hreflang="{{}}"' },
]
}),
// Try value suggestion for a tag-specific attribute
testCompletionFor('<ion-side-menu side="|', {
items: [
{ label: 'left', resultText: '<ion-side-menu side="left"' },
{ label: 'primary', resultText: '<ion-side-menu side="primary"' },
{ label: 'right', resultText: '<ion-side-menu side="right"' },
{ label: 'secondary', resultText: '<ion-side-menu side="secondary"' },
]
}),
// Try a value suggestion for a global attribute
testCompletionFor('<img force-refresh-images="|', {
items: [
{ label: 'false', resultText: '<img force-refresh-images="false"' },
{ label: 'true', resultText: '<img force-refresh-images="true"' },
]
}),
// Try a value suggestion for an extended attribute of an existing HTML 5 tag
testCompletionFor('<a nav-transition="|', {
items: [
{ label: 'android', resultText: '<a nav-transition="android"' },
{ label: 'ios', resultText: '<a nav-transition="ios"' },
{ label: 'none', resultText: '<a nav-transition="none"' },
]
})
], testDone);
});
test('Settings', function (testDone): any {
run([
testCompletionFor('<|', {
items: [
{ label: 'ion-checkbox'},
{ label: 'div', notAvailable: true },
]
}, { html5: false, ionic: true, angular1: false }),
testCompletionFor('<|', {
items: [
{ label: 'ion-checkbox', notAvailable: true },
{ label: 'div' },
]
}, { html5: true, ionic: false, angular1: false }),
testCompletionFor('<input |> </input >', {
items: [
{ label: 'ng-model', notAvailable: true },
{ label: 'type' },
]
}, { html5: true, ionic: false, angular1: false }),
], testDone);
});
})