[html] add embedded JavaScript support

This commit is contained in:
Martin Aeschlimann
2016-11-16 10:58:34 +01:00
parent ab8e320022
commit 03813455b7
14 changed files with 64801 additions and 209 deletions

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* 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 { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import { LanguageService as HTMLLanguageService, HTMLDocument } from 'vscode-html-languageservice';
import { TextDocument, Position } from 'vscode-languageserver-types';
import { getCSSLanguageService, Stylesheet } from 'vscode-css-languageservice';
import { getEmbeddedDocument } from './embeddedSupport';
import { LanguageMode } from './languageModes';
export function getCSSMode(htmlLanguageService: HTMLLanguageService, htmlDocuments: LanguageModelCache<HTMLDocument>): LanguageMode {
let cssLanguageService = getCSSLanguageService();
let cssStylesheets = getLanguageModelCache<Stylesheet>(10, 60, document => cssLanguageService.parseStylesheet(document));
let getEmbeddedCSSDocument = (document: TextDocument) => getEmbeddedDocument(htmlLanguageService, document, htmlDocuments.get(document), 'css');
return {
configure(options: any) {
cssLanguageService.configure(options && options.css);
},
doValidation(document: TextDocument) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded));
},
doComplete(document: TextDocument, position: Position) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.doComplete(embedded, position, cssStylesheets.get(embedded));
},
doHover(document: TextDocument, position: Position) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded));
},
findDocumentHighlight(document: TextDocument, position: Position) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded));
},
findDefinition(document: TextDocument, position: Position) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded));
},
findReferences(document: TextDocument, position: Position) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded));
},
findColorSymbols(document: TextDocument) {
let embedded = getEmbeddedCSSDocument(document);
return cssLanguageService.findColorSymbols(embedded, cssStylesheets.get(embedded));
},
onDocumentRemoved(document: TextDocument) {
cssStylesheets.onDocumentRemoved(document);
},
dispose() {
cssStylesheets.dispose();
}
};
};

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* 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 { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType } from 'vscode-html-languageservice';
export function getLanguageAtPosition(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, position: Position): string {
let offset = document.offsetAt(position);
let node = htmlDocument.findNodeAt(offset);
if (node && node.children.length === 0) {
let embeddedContent = getEmbeddedContentForNode(languageService, document, node);
if (embeddedContent && embeddedContent.start <= offset && offset <= embeddedContent.end) {
return embeddedContent.languageId;
}
}
return 'html';
}
export function getLanguagesInContent(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument): string[] {
let embeddedLanguageIds: { [languageId: string]: boolean } = { html: true };
function collectEmbeddedLanguages(node: Node): void {
let c = getEmbeddedContentForNode(languageService, document, node);
if (c && !isWhitespace(document.getText().substring(c.start, c.end))) {
embeddedLanguageIds[c.languageId] = true;
}
node.children.forEach(collectEmbeddedLanguages);
}
htmlDocument.roots.forEach(collectEmbeddedLanguages);
return Object.keys(embeddedLanguageIds);
}
export function getEmbeddedDocument(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): TextDocument {
let contents = [];
function collectEmbeddedNodes(node: Node): void {
let c = getEmbeddedContentForNode(languageService, document, node);
if (c && c.languageId === languageId) {
contents.push(c);
}
node.children.forEach(collectEmbeddedNodes);
}
htmlDocument.roots.forEach(collectEmbeddedNodes);
let currentPos = 0;
let oldContent = document.getText();
let result = '';
for (let c of contents) {
result = substituteWithWhitespace(result, currentPos, c.start, oldContent);
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
}
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent);
return TextDocument.create(document.uri, languageId, document.version, result);
}
function substituteWithWhitespace(result, start, end, oldContent) {
let accumulatedWS = 0;
for (let i = start; i < end; i++) {
let ch = oldContent[i];
if (ch === '\n' || ch === '\r') {
// only write new lines, skip the whitespace
accumulatedWS = 0;
result += ch;
} else {
accumulatedWS++;
}
}
result = append(result, ' ', accumulatedWS);
return result;
}
function append(result: string, str: string, n: number): string {
while (n) {
if (n & 1) {
result += str;
}
n >>= 1;
str += str;
}
return result;
}
function getEmbeddedContentForNode(languageService: LanguageService, document: TextDocument, node: Node): { languageId: string, start: number, end: number } {
if (node.tag === 'style') {
let scanner = languageService.createScanner(document.getText().substring(node.start, node.end));
let token = scanner.scan();
while (token !== TokenType.EOS) {
if (token === TokenType.Styles) {
return { languageId: 'css', start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() };
}
token = scanner.scan();
}
} else if (node.tag === 'script') {
let scanner = languageService.createScanner(document.getText().substring(node.start, node.end));
let token = scanner.scan();
let isTypeAttribute = false;
let languageId = 'javascript';
while (token !== TokenType.EOS) {
if (token === TokenType.AttributeName) {
isTypeAttribute = scanner.getTokenText() === 'type';
} else if (token === TokenType.AttributeValue) {
if (isTypeAttribute) {
if (/["'](text|application)\/(java|ecma)script["']/.test(scanner.getTokenText())) {
languageId = 'javascript';
} else {
languageId = void 0;
}
}
isTypeAttribute = false;
} else if (token === TokenType.Script) {
return { languageId, start: node.start + scanner.getTokenOffset(), end: node.start + scanner.getTokenEnd() };
}
token = scanner.scan();
}
}
return void 0;
}
function isWhitespace(str: string) {
return str.match(/^\s*$/);
}

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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 { LanguageModelCache } from '../languageModelCache';
import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions } from 'vscode-html-languageservice';
import { TextDocument, Position, Range } from 'vscode-languageserver-types';
import { LanguageMode } from './languageModes';
export function getHTMLMode(htmlLanguageService: HTMLLanguageService, htmlDocuments: LanguageModelCache<HTMLDocument>): LanguageMode {
let settings: any = {};
return {
configure(options: any) {
settings = options && options.html;
},
doComplete(document: TextDocument, position: Position) {
let options = settings && settings.html && settings.html.suggest;
return htmlLanguageService.doComplete(document, position, htmlDocuments.get(document), options);
},
doHover(document: TextDocument, position: Position) {
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
},
findDocumentHighlight(document: TextDocument, position: Position) {
return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document));
},
findDocumentLinks(document: TextDocument, documentContext: DocumentContext) {
return htmlLanguageService.findDocumentLinks(document, documentContext);
},
format(document: TextDocument, range: Range, formatParams: FormattingOptions) {
let formatSettings = settings && settings.format;
if (!formatSettings) {
formatSettings = formatParams;
} else {
formatSettings = merge(formatParams, merge(formatSettings, {}));
}
return htmlLanguageService.format(document, range, formatSettings);
},
onDocumentRemoved(document: TextDocument) {
htmlDocuments.onDocumentRemoved(document);
},
dispose() {
htmlDocuments.dispose();
}
};
};
function merge(src: any, dst: any): any {
for (var key in src) {
if (src.hasOwnProperty(key)) {
dst[key] = src[key];
}
}
return dst;
}

View File

@@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* 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 { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import { LanguageService as HTMLLanguageService, HTMLDocument } from 'vscode-html-languageservice';
import { getEmbeddedDocument } from './embeddedSupport';
import { Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions } from 'vscode-languageserver-types';
import { LanguageMode } from './languageModes';
import ts = require('./typescript/typescriptServices');
import { contents as libdts } from './typescript/lib-ts';
const DEFAULT_LIB = {
NAME: 'defaultLib:lib.d.ts',
CONTENTS: libdts
};
const FILE_NAME = 'typescript://singlefile/1'; // the same 'file' is used for all contents
export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, htmlDocuments: LanguageModelCache<HTMLDocument>): LanguageMode {
let compilerOptions = { allowNonTsExtensions: true, allowJs: true, target: ts.ScriptTarget.Latest };
let currentTextDocument: TextDocument;
let host = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => [FILE_NAME],
getScriptVersion: (fileName: string) => {
if (fileName === FILE_NAME) {
return String(currentTextDocument.version);
}
return '1'; // default lib is static
},
getScriptSnapshot: (fileName: string) => {
let text = fileName === FILE_NAME ? currentTextDocument.getText() : DEFAULT_LIB.CONTENTS;
return {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => void 0
};
},
getCurrentDirectory: () => '',
getDefaultLibFileName: options => DEFAULT_LIB.NAME
};
let jsLanguageService = ts.createLanguageService(host);
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => {
return getEmbeddedDocument(htmlLanguageService, document, htmlDocuments.get(document), 'javascript');
});
let settings: any = {};
return {
configure(options: any) {
settings = options && options.html;
},
doValidation(document: TextDocument): Diagnostic[] {
currentTextDocument = jsDocuments.get(document);
const diagnostics = jsLanguageService.getSyntacticDiagnostics(FILE_NAME);
return diagnostics.map(diag => {
return {
range: convertRange(currentTextDocument, diag),
severity: DiagnosticSeverity.Error,
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
};
});
},
doComplete(document: TextDocument, position: Position): CompletionList {
currentTextDocument = jsDocuments.get(document);
let completions = jsLanguageService.getCompletionsAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
return {
isIncomplete: false,
items: !completions ? [] : completions.entries.map(entry => {
return {
uri: document.uri,
position: position,
label: entry.name,
sortText: entry.sortText,
kind: convertKind(entry.kind)
};
})
};
},
doHover(document: TextDocument, position: Position): Hover {
currentTextDocument = jsDocuments.get(document);
let info = jsLanguageService.getQuickInfoAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (info) {
let contents = ts.displayPartsToString(info.displayParts);
return {
range: convertRange(currentTextDocument, info.textSpan),
contents: MarkedString.fromPlainText(contents)
};
}
return null;
},
doSignatureHelp(document: TextDocument, position: Position): SignatureHelp {
currentTextDocument = jsDocuments.get(document);
let signHelp = jsLanguageService.getSignatureHelpItems(FILE_NAME, currentTextDocument.offsetAt(position));
if (signHelp) {
let ret: SignatureHelp = {
activeSignature: signHelp.selectedItemIndex,
activeParameter: signHelp.argumentIndex,
signatures: []
};
signHelp.items.forEach(item => {
let signature: SignatureInformation = {
label: '',
documentation: null,
parameters: []
};
signature.label += ts.displayPartsToString(item.prefixDisplayParts);
item.parameters.forEach((p, i, a) => {
let label = ts.displayPartsToString(p.displayParts);
let parameter: ParameterInformation = {
label: label,
documentation: ts.displayPartsToString(p.documentation)
};
signature.label += label;
signature.parameters.push(parameter);
if (i < a.length - 1) {
signature.label += ts.displayPartsToString(item.separatorDisplayParts);
}
});
signature.label += ts.displayPartsToString(item.suffixDisplayParts);
ret.signatures.push(signature);
});
return ret;
};
return null;
},
findDocumentHighlight(document: TextDocument, position: Position): DocumentHighlight[] {
currentTextDocument = jsDocuments.get(document);
let occurrences = jsLanguageService.getOccurrencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (occurrences) {
return occurrences.map(entry => {
return {
range: convertRange(currentTextDocument, entry.textSpan),
kind: entry.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Text
};
});
};
return null;
},
findDefinition(document: TextDocument, position: Position): Definition {
currentTextDocument = jsDocuments.get(document);
let definition = jsLanguageService.getDefinitionAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (definition) {
return definition.filter(d => d.fileName === FILE_NAME).map(d => {
return {
uri: document.uri,
range: convertRange(currentTextDocument, d.textSpan)
};
});
}
return null;
},
findReferences(document: TextDocument, position: Position): Location[] {
currentTextDocument = jsDocuments.get(document);
let references = jsLanguageService.getReferencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
if (references) {
return references.filter(d => d.fileName === FILE_NAME).map(d => {
return {
uri: document.uri,
range: convertRange(currentTextDocument, d.textSpan)
};
});
}
return null;
},
format(document: TextDocument, range: Range, formatParams: FormattingOptions): TextEdit[] {
currentTextDocument = jsDocuments.get(document);
let formatSettings = convertOptions(formatParams, settings && settings.format);
let start = currentTextDocument.offsetAt(range.start);
let end = currentTextDocument.offsetAt(range.end);
let edits = jsLanguageService.getFormattingEditsForRange(FILE_NAME, start, end, formatSettings);
if (edits) {
return edits.map(e => ({
range: convertRange(currentTextDocument, e.span),
newText: e.newText
}));
}
return null;
},
onDocumentRemoved(document: TextDocument) {
jsDocuments.onDocumentRemoved(document);
},
dispose() {
jsDocuments.dispose();
jsLanguageService.dispose();
}
};
};
function convertRange(document: TextDocument, span: { start: number, length: number }): Range {
let startPosition = document.positionAt(span.start);
let endPosition = document.positionAt(span.start + span.length);
return Range.create(startPosition, endPosition);
}
function convertKind(kind: string): CompletionItemKind {
switch (kind) {
case 'primitive type':
case 'keyword':
return CompletionItemKind.Keyword;
case 'var':
case 'local var':
return CompletionItemKind.Variable;
case 'property':
case 'getter':
case 'setter':
return CompletionItemKind.Field;
case 'function':
case 'method':
case 'construct':
case 'call':
case 'index':
return CompletionItemKind.Function;
case 'enum':
return CompletionItemKind.Enum;
case 'module':
return CompletionItemKind.Module;
case 'class':
return CompletionItemKind.Class;
case 'interface':
return CompletionItemKind.Interface;
case 'warning':
return CompletionItemKind.File;
}
return CompletionItemKind.Property;
}
function convertOptions(options: FormattingOptions, formatSettings?: any): ts.FormatCodeOptions {
return {
ConvertTabsToSpaces: options.insertSpaces,
TabSize: options.tabSize,
IndentSize: options.tabSize,
IndentStyle: ts.IndentStyle.Smart,
NewLineCharacter: '\n',
BaseIndentSize: 1, //
InsertSpaceAfterCommaDelimiter: !formatSettings || formatSettings.insertSpaceAfterCommaDelimiter,
InsertSpaceAfterSemicolonInForStatements: !formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements,
InsertSpaceBeforeAndAfterBinaryOperators: !formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators,
InsertSpaceAfterKeywordsInControlFlowStatements: !formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements,
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: !formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets,
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces,
PlaceOpenBraceOnNewLineForControlBlocks: formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions,
PlaceOpenBraceOnNewLineForFunctions: formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks
};
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* 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 { HTMLDocument, getLanguageService as getHTMLLanguageService, DocumentContext } from 'vscode-html-languageservice';
import { Location, SignatureHelp, Definition, TextEdit, TextDocument, Diagnostic, DocumentLink, Range, Hover, DocumentHighlight, CompletionList, Position, FormattingOptions } from 'vscode-languageserver-types';
import { getLanguageModelCache } from '../languageModelCache';
import { getLanguageAtPosition, getLanguagesInContent } from './embeddedSupport';
import { getCSSMode } from './cssMode';
import { getJavascriptMode } from './javascriptMode';
import { getHTMLMode } from './htmlMode';
export interface LanguageMode {
configure?: (options: any) => void;
doValidation?: (document: TextDocument) => Diagnostic[];
doComplete?: (document: TextDocument, position: Position) => CompletionList;
doHover?: (document: TextDocument, position: Position) => Hover;
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp;
findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[];
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[];
findDefinition?: (document: TextDocument, position: Position) => Definition;
findReferences?: (document: TextDocument, position: Position) => Location[];
format?: (document: TextDocument, range: Range, options: FormattingOptions) => TextEdit[];
findColorSymbols?: (document: TextDocument) => Range[];
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}
export interface LanguageModes {
getModeAtPosition(document: TextDocument, position: Position): LanguageMode;
getAllModesInDocument(document: TextDocument): LanguageMode[];
getAllModes(): LanguageMode[];
getMode(languageId: string): LanguageMode;
}
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }): LanguageModes {
var htmlLanguageService = getHTMLLanguageService();
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
let modes = {
'html': getHTMLMode(htmlLanguageService, htmlDocuments),
'css': supportedLanguages['css'] && getCSSMode(htmlLanguageService, htmlDocuments),
'javascript': supportedLanguages['javascript'] && getJavascriptMode(htmlLanguageService, htmlDocuments)
};
return {
getModeAtPosition(document: TextDocument, position: Position): LanguageMode {
let languageId = getLanguageAtPosition(htmlLanguageService, document, htmlDocuments.get(document), position);
if (languageId) {
return modes[languageId];
}
return null;
},
getAllModesInDocument(document: TextDocument): LanguageMode[] {
let result = [];
let languageIds = getLanguagesInContent(htmlLanguageService, document, htmlDocuments.get(document));
for (let languageId of languageIds) {
let mode = modes[languageId];
if (mode) {
result.push(mode);
}
}
return result;
},
getAllModes(): LanguageMode[] {
let result = [];
for (let languageId in modes) {
let mode = modes[languageId];
if (mode) {
result.push(mode);
}
}
return result;
},
getMode(languageId: string): LanguageMode {
return modes[languageId];
}
};
}

View File

@@ -0,0 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export declare var contents: string;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long