[html] bring back provider configuration settings
This commit is contained in:
parent
cf1596c6f0
commit
6d788bff70
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue