mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
[html] Format embedded JavaScript
This commit is contained in:
parent
e51299d9cb
commit
cbdddca5ed
8
extensions/html/.vscode/launch.json
vendored
8
extensions/html/.vscode/launch.json
vendored
|
@ -24,6 +24,14 @@
|
|||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/client/out/test",
|
||||
"preLaunchTask": "npm"
|
||||
},
|
||||
{
|
||||
"name": "Attach Language Server",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 6004,
|
||||
"sourceMaps": true,
|
||||
"outDir": "${workspaceRoot}/server/out"
|
||||
}
|
||||
]
|
||||
}
|
4
extensions/html/server/npm-shrinkwrap.json
generated
4
extensions/html/server/npm-shrinkwrap.json
generated
|
@ -8,9 +8,9 @@
|
|||
"resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.0.0-next.3.tgz"
|
||||
},
|
||||
"vscode-html-languageservice": {
|
||||
"version": "1.0.1-next.2",
|
||||
"version": "1.0.1-next.3",
|
||||
"from": "vscode-html-languageservice@next",
|
||||
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.2.tgz"
|
||||
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-1.0.1-next.3.tgz"
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "2.4.0",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"vscode-css-languageservice": "^2.0.0-next.3",
|
||||
"vscode-html-languageservice": "^1.0.1-next.2",
|
||||
"vscode-html-languageservice": "^1.0.1-next.3",
|
||||
"vscode-languageserver": "^2.6.2-next.1",
|
||||
"vscode-nls": "^1.0.4",
|
||||
"vscode-uri": "^1.0.0"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
'use strict';
|
||||
|
||||
import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType } from 'vscode-languageserver';
|
||||
import { DocumentContext, TextDocument, Diagnostic, DocumentLink, Range } from 'vscode-html-languageservice';
|
||||
import { DocumentContext, TextDocument, Diagnostic, DocumentLink, Range, TextEdit } from 'vscode-html-languageservice';
|
||||
import { getLanguageModes, LanguageModes } from './modes/languageModes';
|
||||
|
||||
import * as url from 'url';
|
||||
|
@ -112,12 +112,20 @@ function validateTextDocument(textDocument: TextDocument): void {
|
|||
let diagnostics: Diagnostic[] = [];
|
||||
languageModes.getAllModesInDocument(textDocument).forEach(mode => {
|
||||
if (mode.doValidation) {
|
||||
diagnostics = diagnostics.concat(mode.doValidation(textDocument));
|
||||
pushAll(diagnostics, mode.doValidation(textDocument));
|
||||
}
|
||||
});
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
|
||||
}
|
||||
|
||||
function pushAll<T>(to: T[], from: T[]) {
|
||||
if (from) {
|
||||
for (var i = 0; i < from.length; i++) {
|
||||
to.push(from[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.onCompletion(textDocumentPosition => {
|
||||
let document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
let mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
|
@ -186,12 +194,16 @@ connection.onSignatureHelp(signatureHelpParms => {
|
|||
|
||||
connection.onDocumentRangeFormatting(formatParams => {
|
||||
let document = documents.get(formatParams.textDocument.uri);
|
||||
let startMode = languageModes.getModeAtPosition(document, formatParams.range.start);
|
||||
let endMode = languageModes.getModeAtPosition(document, formatParams.range.end);
|
||||
if (startMode && startMode === endMode && startMode.format) {
|
||||
return startMode.format(document, formatParams.range, formatParams.options);
|
||||
}
|
||||
return null;
|
||||
let ranges = languageModes.getModesInRange(document, formatParams.range);
|
||||
let result: TextEdit[] = [];
|
||||
ranges.forEach(r => {
|
||||
let mode = r.mode;
|
||||
if (mode && mode.format) {
|
||||
let edits = mode.format(document, r, formatParams.options);
|
||||
pushAll(result, edits);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
connection.onDocumentLinks(documentLinkParam => {
|
||||
|
@ -207,7 +219,7 @@ connection.onDocumentLinks(documentLinkParam => {
|
|||
let links: DocumentLink[] = [];
|
||||
languageModes.getAllModesInDocument(document).forEach(m => {
|
||||
if (m.findDocumentLinks) {
|
||||
links = links.concat(m.findDocumentLinks(document, documentContext));
|
||||
pushAll(links, m.findDocumentLinks(document, documentContext));
|
||||
}
|
||||
});
|
||||
return links;
|
||||
|
@ -219,7 +231,7 @@ connection.onRequest(ColorSymbolRequest.type, uri => {
|
|||
if (document) {
|
||||
languageModes.getAllModesInDocument(document).forEach(m => {
|
||||
if (m.findColorSymbols) {
|
||||
ranges = ranges.concat(m.findColorSymbols(document));
|
||||
pushAll(ranges, m.findColorSymbols(document));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType } from 'vscode-html-languageservice';
|
||||
import { TextDocument, Position, HTMLDocument, Node, LanguageService, TokenType, Range } from 'vscode-html-languageservice';
|
||||
|
||||
export interface LanguageRange extends Range {
|
||||
languageId: string;
|
||||
}
|
||||
|
||||
export function getLanguageAtPosition(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, position: Position): string {
|
||||
let offset = document.offsetAt(position);
|
||||
|
@ -33,6 +37,50 @@ export function getLanguagesInContent(languageService: LanguageService, document
|
|||
return Object.keys(embeddedLanguageIds);
|
||||
}
|
||||
|
||||
export function getLanguagesInRange(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, range: Range): LanguageRange[] {
|
||||
let ranges: LanguageRange[] = [];
|
||||
let currentPos = range.start;
|
||||
let currentOffset = document.offsetAt(currentPos);
|
||||
let rangeEndOffset = document.offsetAt(range.end);
|
||||
function collectEmbeddedNodes(node: Node): void {
|
||||
if (node.start < rangeEndOffset && node.end > currentOffset) {
|
||||
let c = getEmbeddedContentForNode(languageService, document, node);
|
||||
if (c && c.start < rangeEndOffset) {
|
||||
let startPos = document.positionAt(c.start);
|
||||
if (currentOffset < c.start) {
|
||||
ranges.push({
|
||||
start: currentPos,
|
||||
end: startPos,
|
||||
languageId: 'html'
|
||||
});
|
||||
}
|
||||
let end = Math.min(c.end, rangeEndOffset);
|
||||
let endPos = document.positionAt(end);
|
||||
if (end > c.start) {
|
||||
ranges.push({
|
||||
start: startPos,
|
||||
end: endPos,
|
||||
languageId: c.languageId
|
||||
});
|
||||
}
|
||||
currentOffset = end;
|
||||
currentPos = endPos;
|
||||
}
|
||||
}
|
||||
node.children.forEach(collectEmbeddedNodes);
|
||||
}
|
||||
|
||||
htmlDocument.roots.forEach(collectEmbeddedNodes);
|
||||
if (currentOffset < rangeEndOffset) {
|
||||
ranges.push({
|
||||
start: currentPos,
|
||||
end: range.end,
|
||||
languageId: 'html'
|
||||
});
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
export function getEmbeddedDocument(languageService: LanguageService, document: TextDocument, htmlDocument: HTMLDocument, languageId: string): TextDocument {
|
||||
let contents = [];
|
||||
function collectEmbeddedNodes(node: Node): void {
|
||||
|
|
|
@ -54,7 +54,7 @@ export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, html
|
|||
|
||||
return {
|
||||
configure(options: any) {
|
||||
settings = options && options.html;
|
||||
settings = options && options.javascript;
|
||||
},
|
||||
doValidation(document: TextDocument): Diagnostic[] {
|
||||
currentTextDocument = jsDocuments.get(document);
|
||||
|
@ -194,15 +194,22 @@ export function getJavascriptMode(htmlLanguageService: HTMLLanguageService, html
|
|||
},
|
||||
format(document: TextDocument, range: Range, formatParams: FormattingOptions): TextEdit[] {
|
||||
currentTextDocument = jsDocuments.get(document);
|
||||
let formatSettings = convertOptions(formatParams, settings && settings.format);
|
||||
let initialIndentLevel = computeInitialIndent(document, range, formatParams) + 1;
|
||||
let formatSettings = convertOptions(formatParams, settings && settings.format, initialIndentLevel);
|
||||
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
|
||||
}));
|
||||
let result = [];
|
||||
for (let edit of edits) {
|
||||
if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
|
||||
result.push({
|
||||
range: convertRange(currentTextDocument, edit.span),
|
||||
newText: edit.newText
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
@ -255,23 +262,44 @@ function convertKind(kind: string): CompletionItemKind {
|
|||
return CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
function convertOptions(options: FormattingOptions, formatSettings?: any): ts.FormatCodeOptions {
|
||||
function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): 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
|
||||
BaseIndentSize: options.tabSize * initialIndentLevel,
|
||||
InsertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter),
|
||||
InsertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements),
|
||||
InsertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators),
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements),
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces),
|
||||
PlaceOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions),
|
||||
PlaceOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks)
|
||||
};
|
||||
}
|
||||
|
||||
function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) {
|
||||
let lineStart = document.offsetAt(Position.create(range.start.line, 0));
|
||||
let content = document.getText();
|
||||
|
||||
let i = lineStart;
|
||||
let nChars = 0;
|
||||
let tabSize = options.tabSize || 4;
|
||||
while (i < content.length) {
|
||||
let ch = content.charAt(i);
|
||||
if (ch === ' ') {
|
||||
nChars++;
|
||||
} else if (ch === '\t') {
|
||||
nChars += tabSize;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return Math.floor(nChars / tabSize);
|
||||
}
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'vscode-languageserver-types';
|
||||
|
||||
import { getLanguageModelCache } from '../languageModelCache';
|
||||
import { getLanguageAtPosition, getLanguagesInContent } from './embeddedSupport';
|
||||
import { getLanguageAtPosition, getLanguagesInContent, getLanguagesInRange } from './embeddedSupport';
|
||||
import { getCSSMode } from './cssMode';
|
||||
import { getJavascriptMode } from './javascriptMode';
|
||||
import { getHTMLMode } from './htmlMode';
|
||||
|
@ -35,11 +35,16 @@ export interface LanguageMode {
|
|||
|
||||
export interface LanguageModes {
|
||||
getModeAtPosition(document: TextDocument, position: Position): LanguageMode;
|
||||
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
|
||||
getAllModesInDocument(document: TextDocument): LanguageMode[];
|
||||
getAllModes(): LanguageMode[];
|
||||
getMode(languageId: string): LanguageMode;
|
||||
}
|
||||
|
||||
export interface LanguageModeRange extends Range {
|
||||
mode: LanguageMode;
|
||||
}
|
||||
|
||||
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }): LanguageModes {
|
||||
|
||||
var htmlLanguageService = getHTMLLanguageService();
|
||||
|
@ -69,6 +74,15 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo
|
|||
}
|
||||
return result;
|
||||
},
|
||||
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] {
|
||||
return getLanguagesInRange(htmlLanguageService, document, htmlDocuments.get(document), range).map(r => {
|
||||
return {
|
||||
start: r.start,
|
||||
end: r.end,
|
||||
mode: modes[r.languageId]
|
||||
};
|
||||
});
|
||||
},
|
||||
getAllModes(): LanguageMode[] {
|
||||
let result = [];
|
||||
for (let languageId in modes) {
|
||||
|
|
108
extensions/html/server/src/test/formatting.test.ts
Normal file
108
extensions/html/server/src/test/formatting.test.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as assert from 'assert';
|
||||
import { getLanguageModes } from '../modes/languageModes';
|
||||
import { TextDocument, Range, TextEdit, FormattingOptions } from 'vscode-languageserver-types';
|
||||
|
||||
suite('HTML Embedded Formatting', () => {
|
||||
|
||||
function assertFormat(value: string, expected: string, options?: any): void {
|
||||
var languageModes = getLanguageModes({ css: true, javascript: true });
|
||||
if (options) {
|
||||
languageModes.getAllModes().forEach(m => m.configure(options));
|
||||
}
|
||||
|
||||
let rangeStartOffset = value.indexOf('|');
|
||||
let rangeEndOffset;
|
||||
if (rangeStartOffset !== -1) {
|
||||
value = value.substr(0, rangeStartOffset) + value.substr(rangeStartOffset + 1);
|
||||
|
||||
rangeEndOffset = value.indexOf('|');
|
||||
value = value.substr(0, rangeEndOffset) + value.substr(rangeEndOffset + 1);
|
||||
} else {
|
||||
rangeStartOffset = 0;
|
||||
rangeEndOffset = value.length;
|
||||
}
|
||||
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
let range = Range.create(document.positionAt(rangeStartOffset), document.positionAt(rangeEndOffset));
|
||||
let formatOptions = FormattingOptions.create(2, true);
|
||||
|
||||
let ranges = languageModes.getModesInRange(document, range);
|
||||
let result: TextEdit[] = [];
|
||||
ranges.forEach(r => {
|
||||
let mode = r.mode;
|
||||
if (mode && mode.format) {
|
||||
let edits = mode.format(document, r, formatOptions);
|
||||
pushAll(result, edits);
|
||||
}
|
||||
});
|
||||
let actual = applyEdits(document, result);
|
||||
assert.equal(actual, expected);
|
||||
}
|
||||
|
||||
test('HTML only', function (): any {
|
||||
assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
assertFormat('|<html><body><p>Hello</p></body></html>|', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>');
|
||||
});
|
||||
|
||||
test('HTML & Scripts', function (): any {
|
||||
assertFormat('<html><head><script></script></head></html>', '<html>\n\n<head>\n <script></script>\n</head>\n\n</html>');
|
||||
assertFormat('<html><head><script>var x=1;</script></head></html>', '<html>\n\n<head>\n <script>var x = 1;</script>\n</head>\n\n</html>');
|
||||
assertFormat('<html><head><script>\nvar x=1;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 1;\n</script>\n</head>\n\n</html>');
|
||||
assertFormat('<html><head>\n <script>\nvar x=1;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 1;\n</script>\n</head>\n\n</html>');
|
||||
assertFormat('<html><head>\n <script>\nvar x=1;\nconsole.log("Hi");\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 1;\n console.log("Hi");\n</script>\n</head>\n\n</html>');
|
||||
|
||||
assertFormat('<html><head>\n |<script>\nvar x=1;\n</script>|</head></html>', '<html><head>\n <script>\n var x = 1;\n</script></head></html>');
|
||||
assertFormat('<html><head>\n <script>\n|var x=1;|\n</script></head></html>', '<html><head>\n <script>\n var x = 1;\n</script></head></html>');
|
||||
});
|
||||
|
||||
test('HTML & Multiple Scripts', function (): any {
|
||||
assertFormat('<html><head>\n<script>\nif(x){\nbar(); }\n</script><script>\nfunction(x){}\n</script></head></html>', '<html>\n\n<head>\n <script>\n if (x) {\n bar();\n }\n</script>\n<script>\n function(x) { }\n</script>\n</head>\n\n</html>');
|
||||
});
|
||||
|
||||
test('HTML & Styles', function (): any {
|
||||
assertFormat('<html><head>\n<style>\n.foo{display:none;}\n</style></head></html>', '<html>\n\n<head>\n <style>\n.foo{display:none;}\n</style>\n</head>\n\n</html>');
|
||||
});
|
||||
|
||||
test('EndWithNewline', function (): any {
|
||||
let options = {
|
||||
html: {
|
||||
format: {
|
||||
endWithNewline : true
|
||||
}
|
||||
}
|
||||
};
|
||||
assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>\n', options);
|
||||
assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>', options);
|
||||
assertFormat('<html><head><script>\nvar x=1;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 1;\n</script>\n</head>\n\n</html>\n', options);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function pushAll<T>(to: T[], from: T[]) {
|
||||
if (from) {
|
||||
for (var i = 0; i < from.length; i++) {
|
||||
to.push(from[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyEdits(document: TextDocument, edits: TextEdit[]): string {
|
||||
let text = document.getText();
|
||||
let sortedEdits = edits.sort((a, b) => document.offsetAt(b.range.start) - document.offsetAt(a.range.start));
|
||||
let lastOffset = text.length;
|
||||
sortedEdits.forEach(e => {
|
||||
let startOffset = document.offsetAt(e.range.start);
|
||||
let endOffset = document.offsetAt(e.range.end);
|
||||
assert.ok(startOffset <= endOffset);
|
||||
assert.ok(endOffset <= lastOffset);
|
||||
text = text.substring(0, startOffset) + e.newText + text.substring(endOffset, text.length);
|
||||
lastOffset = startOffset;
|
||||
});
|
||||
return text;
|
||||
}
|
Loading…
Reference in a new issue