diff --git a/extensions/html/.vscode/launch.json b/extensions/html/.vscode/launch.json
index 7a654d67e8d..3b0e212f68e 100644
--- a/extensions/html/.vscode/launch.json
+++ b/extensions/html/.vscode/launch.json
@@ -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"
}
]
}
\ No newline at end of file
diff --git a/extensions/html/server/npm-shrinkwrap.json b/extensions/html/server/npm-shrinkwrap.json
index 811b781c29d..a1f7d09a5e9 100644
--- a/extensions/html/server/npm-shrinkwrap.json
+++ b/extensions/html/server/npm-shrinkwrap.json
@@ -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",
diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json
index ccda47decda..8dbee89cebb 100644
--- a/extensions/html/server/package.json
+++ b/extensions/html/server/package.json
@@ -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"
diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts
index c4adb4a7633..796b7b8ae78 100644
--- a/extensions/html/server/src/htmlServerMain.ts
+++ b/extensions/html/server/src/htmlServerMain.ts
@@ -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(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));
}
});
}
diff --git a/extensions/html/server/src/modes/embeddedSupport.ts b/extensions/html/server/src/modes/embeddedSupport.ts
index e982527a89e..9608696b271 100644
--- a/extensions/html/server/src/modes/embeddedSupport.ts
+++ b/extensions/html/server/src/modes/embeddedSupport.ts
@@ -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 {
diff --git a/extensions/html/server/src/modes/javascriptMode.ts b/extensions/html/server/src/modes/javascriptMode.ts
index 19edf0a0d5a..957db80b184 100644
--- a/extensions/html/server/src/modes/javascriptMode.ts
+++ b/extensions/html/server/src/modes/javascriptMode.ts
@@ -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);
}
\ No newline at end of file
diff --git a/extensions/html/server/src/modes/languageModes.ts b/extensions/html/server/src/modes/languageModes.ts
index 1e52531b1e1..4e788b7bf51 100644
--- a/extensions/html/server/src/modes/languageModes.ts
+++ b/extensions/html/server/src/modes/languageModes.ts
@@ -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) {
diff --git a/extensions/html/server/src/test/formatting.test.ts b/extensions/html/server/src/test/formatting.test.ts
new file mode 100644
index 00000000000..1aaf6075848
--- /dev/null
+++ b/extensions/html/server/src/test/formatting.test.ts
@@ -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('Hello
', '\n\n\n Hello
\n\n\n');
+ assertFormat('|Hello
|', '\n\n\n Hello
\n\n\n');
+ assertFormat('|Hello
|', '\n Hello
\n');
+ });
+
+ test('HTML & Scripts', function (): any {
+ assertFormat('', '\n\n\n \n\n\n');
+ assertFormat('', '\n\n\n \n\n\n');
+ assertFormat('', '\n\n\n \n\n\n');
+ assertFormat('\n ', '\n\n\n \n\n\n');
+ assertFormat('\n ', '\n\n\n \n\n\n');
+
+ assertFormat('\n ||', '\n ');
+ assertFormat('\n ', '\n ');
+ });
+
+ test('HTML & Multiple Scripts', function (): any {
+ assertFormat('\n', '\n\n\n \n\n\n\n');
+ });
+
+ test('HTML & Styles', function (): any {
+ assertFormat('\n', '\n\n\n \n\n\n');
+ });
+
+ test('EndWithNewline', function (): any {
+ let options = {
+ html: {
+ format: {
+ endWithNewline : true
+ }
+ }
+ };
+ assertFormat('Hello
', '\n\n\n Hello
\n\n\n\n', options);
+ assertFormat('|Hello
|', '\n Hello
\n', options);
+ assertFormat('', '\n\n\n \n\n\n\n', options);
+ });
+
+});
+
+function pushAll(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;
+}
\ No newline at end of file