Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -0,0 +1,43 @@
{
"version": "0.2.0",
"compounds": [
{
"name": "Debug Extension and Language Server",
"configurations": ["Launch Extension", "Attach Language Server"]
}
],
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/client/out/**/*.js"]
},
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/client/out/test" ],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/client/out/test/**/*.js"]
},
{
"name": "Attach Language Server",
"type": "node",
"request": "attach",
"port": 6045,
"protocol": "inspector",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/server/out/**/*.js"],
"restart": true
}
]
}

View File

@ -0,0 +1,6 @@
{
"editor.insertSpaces": false,
"prettier.semi": true,
"prettier.singleQuote": true,
"prettier.printWidth": 120,
}

View File

@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm",
"command": "npm",
"args": ["run", "compile"],
"type": "shell",
"presentation": {
"reveal": "silent",
"focus": false,
"panel": "shared"
},
"isBackground": true,
"problemMatcher": "$tsc-watch"
}
],
}

View File

@ -0,0 +1,22 @@
test/**
.vscode/**
server/.vscode/**
server/node_modules/**
client/src/**
server/src/**
client/out/**
server/out/**
client/tsconfig.json
server/tsconfig.json
server/test/**
server/bin/**
server/build/**
server/yarn.lock
server/.npmignore
yarn.lock
server/extension.webpack.config.js
extension.webpack.config.js
server/extension-browser.webpack.config.js
extension-browser.webpack.config.js
CONTRIBUTING.md
cgmanifest.json

View File

@ -0,0 +1,37 @@
## Setup
- Clone [microsoft/vscode](https://github.com/microsoft/vscode)
- Run `yarn` at `/`, this will install
- Dependencies for `/extension/html-language-features/`
- Dependencies for `/extension/html-language-features/server/`
- devDependencies such as `gulp`
- Open `/extensions/html-language-features/` as the workspace in VS Code
- Run the [`Launch Extension`](https://github.com/microsoft/vscode/blob/master/extensions/html-language-features/.vscode/launch.json) debug target in the Debug View. This will:
- Launch the `preLaunchTask` task to compile the extension
- Launch a new VS Code instance with the `html-language-features` extension loaded
- You should see a notification saying the development version of `html-language-features` overwrites the bundled version of `html-language-features`
- Test the behavior of this extension by editing html files
- Run `Reload Window` command in the launched instance to reload the extension
### Contribute to vscode-html-languageservice
[microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice) contains the language smarts for html.
This extension wraps the html language service into a Language Server for VS Code.
If you want to fix html issues or make improvements, you should make changes at [microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice).
However, within this extension, you can run a development version of `vscode-html-languageservice` to debug code or test language features interactively:
#### Linking `vscode-html-languageservice` in `html-language-features/server/`
- Clone [microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice)
- Run `yarn` in `vscode-html-languageservice`
- Run `yarn link` in `vscode-html-languageservice`. This will compile and link `vscode-html-languageservice`
- In `html-language-features/server/`, run `npm link vscode-html-languageservice`
#### Testing the development version of `vscode-html-languageservice`
- Open both `vscode-html-languageservice` and this extension in a single workspace with [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) feature
- Run `yarn watch` at `html-languagefeatures/server/` to recompile this extension with the linked version of `vscode-html-languageservice`
- Make some changes in `vscode-html-languageservice`
- Now when you run `Launch Extension` debug target, the launched instance will use your development version of `vscode-html-languageservice`. You can interactively test the language features.
- You can also run the `Debug Extension and Language Server` debug target, which will launch the extension and attach the debugger to the language server. After successful attach, you should be able to hit breakpoints in both `vscode-html-languageservice` and `html-language-features/server/`

View File

@ -0,0 +1,9 @@
# Language Features for HTML
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
See [HTML in Visual Studio Code](https://code.visualstudio.com/docs/languages/html) to learn about the features of this extension.
Please read the [CONTRIBUTING.md](https://github.com/microsoft/vscode/blob/master/extensions/html-language-features/CONTRIBUTING.md) file to learn how to contribute to this extension.

View File

@ -0,0 +1,183 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const fs = require('fs');
const child_process = require('child_process');
const generatedNote = `//
// **NOTE**: Do not edit directly! This file is generated using \`npm run import-typescript\`
//
`;
const TYPESCRIPT_LIB_SOURCE = path.join(__dirname, '../../node_modules/typescript/lib');
const TYPESCRIPT_LIB_DESTINATION = path.join(__dirname, '../server/build');
(function () {
try {
fs.statSync(TYPESCRIPT_LIB_DESTINATION);
} catch (err) {
fs.mkdirSync(TYPESCRIPT_LIB_DESTINATION);
}
importLibs('es6');
})();
function importLibs(startLib) {
function getFileName(name) {
return (name === '' ? 'lib.d.ts' : `lib.${name}.d.ts`);
}
function getVariableName(name) {
return (name === '' ? 'lib_dts' : `lib_${name.replace(/\./g, '_')}_dts`);
}
function readLibFile(name) {
var srcPath = path.join(TYPESCRIPT_LIB_SOURCE, getFileName(name));
return fs.readFileSync(srcPath).toString();
}
var queue = [];
var in_queue = {};
var enqueue = function (name) {
if (in_queue[name]) {
return;
}
in_queue[name] = true;
queue.push(name);
};
enqueue(startLib);
var result = [];
while (queue.length > 0) {
var name = queue.shift();
var contents = readLibFile(name);
var lines = contents.split(/\r\n|\r|\n/);
var output = '';
var writeOutput = function (text) {
if (output.length === 0) {
output = text;
} else {
output += ` + ${text}`;
}
};
var outputLines = [];
var flushOutputLines = function () {
writeOutput(`"${escapeText(outputLines.join('\n'))}"`);
outputLines = [];
};
var deps = [];
for (let i = 0; i < lines.length; i++) {
let m = lines[i].match(/\/\/\/\s*<reference\s*lib="([^"]+)"/);
if (m) {
flushOutputLines();
writeOutput(getVariableName(m[1]));
deps.push(getVariableName(m[1]));
enqueue(m[1]);
continue;
}
outputLines.push(lines[i]);
}
flushOutputLines();
result.push({
name: getVariableName(name),
deps: deps,
output: output
});
}
var strResult = `/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
${generatedNote}`;
// Do a topological sort
while (result.length > 0) {
for (let i = result.length - 1; i >= 0; i--) {
if (result[i].deps.length === 0) {
// emit this node
strResult += `\nexport const ${result[i].name}: string = ${result[i].output};\n`;
// mark dep as resolved
for (let j = 0; j < result.length; j++) {
for (let k = 0; k < result[j].deps.length; k++) {
if (result[j].deps[k] === result[i].name) {
result[j].deps.splice(k, 1);
break;
}
}
}
// remove from result
result.splice(i, 1);
break;
}
}
}
var dstPath = path.join(TYPESCRIPT_LIB_DESTINATION, 'lib.ts');
fs.writeFileSync(dstPath, strResult);
}
/**
* Escape text such that it can be used in a javascript string enclosed by double quotes (")
*/
function escapeText(text) {
// See http://www.javascriptkit.com/jsref/escapesequence.shtml
var _backspace = '\b'.charCodeAt(0);
var _formFeed = '\f'.charCodeAt(0);
var _newLine = '\n'.charCodeAt(0);
var _nullChar = 0;
var _carriageReturn = '\r'.charCodeAt(0);
var _tab = '\t'.charCodeAt(0);
var _verticalTab = '\v'.charCodeAt(0);
var _backslash = '\\'.charCodeAt(0);
var _doubleQuote = '"'.charCodeAt(0);
var startPos = 0, chrCode, replaceWith = null, resultPieces = [];
for (var i = 0, len = text.length; i < len; i++) {
chrCode = text.charCodeAt(i);
switch (chrCode) {
case _backspace:
replaceWith = '\\b';
break;
case _formFeed:
replaceWith = '\\f';
break;
case _newLine:
replaceWith = '\\n';
break;
case _nullChar:
replaceWith = '\\0';
break;
case _carriageReturn:
replaceWith = '\\r';
break;
case _tab:
replaceWith = '\\t';
break;
case _verticalTab:
replaceWith = '\\v';
break;
case _backslash:
replaceWith = '\\\\';
break;
case _doubleQuote:
replaceWith = '\\"';
break;
}
if (replaceWith !== null) {
resultPieces.push(text.substring(startPos, i));
resultPieces.push(replaceWith);
startPos = i + 1;
replaceWith = null;
}
}
resultPieces.push(text.substring(startPos, len));
return resultPieces.join('');
}

View File

@ -0,0 +1,115 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "js-beautify",
"repositoryUrl": "https://github.com/beautify-web/js-beautify",
"commitHash": "12e73365f9d0b203843c5b7c22d7017845a7c580"
}
},
"license": "MIT",
"version": "1.6.8"
},
{
"component": {
"type": "other",
"other": {
"name": "HTML 5.1 W3C Working Draft",
"downloadUrl": "http://www.w3.org/TR/2015/WD-html51-20151008/",
"version": "08 October 2015"
}
},
"licenseDetail": [
"Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang). This software or document includes material copied ",
"from or derived from HTML 5.1 W3C Working Draft (http://www.w3.org/TR/2015/WD-html51-20151008/.)",
"",
"THIS DOCUMENT IS PROVIDED \"AS IS,\" AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT ",
"NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS OF ",
"THE DOCUMENT ARE SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY ",
"PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.",
"",
"COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE ",
"DOCUMENT OR THE PERFORMANCE OR IMPLEMENTATION OF THE CONTENTS THEREOF.",
"",
"The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to this document or its contents",
"without specific, written prior permission. Title to copyright in this document will at all times remain with copyright holders."
],
"license": "W3C Document License",
"version": "08 October 2015"
},
{
"component": {
"type": "git",
"git": {
"name": "Ionic documentation",
"repositoryUrl": "https://github.com/ionic-team/ionic-site",
"commitHash": "e952bde103470738e19a456ec4acb0f1e650b619"
}
},
"licenseDetail": [
"Copyright Drifty Co. http://drifty.com/.",
"",
"Apache License",
"",
"Version 2.0, January 2004",
"",
"http://www.apache.org/licenses/",
"",
"TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION",
"",
"1. Definitions.",
"",
"\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.",
"",
"\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.",
"",
"\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.",
"",
"\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.",
"",
"\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.",
"",
"\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.",
"",
"\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).",
"",
"\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.",
"",
"\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"",
"",
"\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.",
"",
"2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.",
"",
"3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.",
"",
"4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:",
"",
"You must give any other recipients of the Work or Derivative Works a copy of this License; and",
"",
"You must cause any modified files to carry prominent notices stating that You changed the files; and",
"",
"You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and",
"",
"If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.",
"",
"5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.",
"",
"6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.",
"",
"7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.",
"",
"8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.",
"",
"9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.",
"",
"END OF TERMS AND CONDITIONS"
],
"license": "Apache2",
"version": "1.2.4"
}
],
"version": 1
}

View File

@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, Uri } from 'vscode';
import { LanguageClientOptions } from 'vscode-languageclient';
import { startClient, LanguageClientConstructor } from '../htmlClient';
import { LanguageClient } from 'vscode-languageclient/browser';
declare const Worker: {
new(stringUrl: string): any;
};
declare const TextDecoder: {
new(encoding?: string): { decode(buffer: ArrayBuffer): string; };
};
// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/htmlServerMain.js');
try {
const worker = new Worker(serverMain.toString());
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, clientOptions, worker);
};
startClient(context, newLanguageClient, { TextDecoder });
} catch (e) {
console.log(e);
}
}

View File

@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
import { resolvePath, joinPath } from './requests';
export function getCustomDataSource(toDispose: Disposable[]) {
let pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
let pathsInExtensions = getCustomDataPathsFromAllExtensions();
const onChange = new EventEmitter<void>();
toDispose.push(extensions.onDidChange(_ => {
const newPathsInExtensions = getCustomDataPathsFromAllExtensions();
if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) {
pathsInExtensions = newPathsInExtensions;
onChange.fire();
}
}));
toDispose.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('html.customData')) {
pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
onChange.fire();
}
}));
return {
get uris() {
return pathsInWorkspace.concat(pathsInExtensions);
},
get onDidChange() {
return onChange.event;
}
};
}
function getCustomDataPathsInAllWorkspaces(): string[] {
const workspaceFolders = workspace.workspaceFolders;
const dataPaths: string[] = [];
if (!workspaceFolders) {
return dataPaths;
}
const collect = (paths: string[] | undefined, rootFolder: Uri) => {
if (Array.isArray(paths)) {
for (const path of paths) {
if (typeof path === 'string') {
dataPaths.push(resolvePath(rootFolder, path).toString());
}
}
}
};
for (let i = 0; i < workspaceFolders.length; i++) {
const folderUri = workspaceFolders[i].uri;
const allHtmlConfig = workspace.getConfiguration('html', folderUri);
const customDataInspect = allHtmlConfig.inspect<string[]>('customData');
if (customDataInspect) {
collect(customDataInspect.workspaceFolderValue, folderUri);
if (i === 0) {
if (workspace.workspaceFile) {
collect(customDataInspect.workspaceValue, workspace.workspaceFile);
}
collect(customDataInspect.globalValue, folderUri);
}
}
}
return dataPaths;
}
function getCustomDataPathsFromAllExtensions(): string[] {
const dataPaths: string[] = [];
for (const extension of extensions.all) {
const customData = extension.packageJSON?.contributes?.html?.customData;
if (Array.isArray(customData)) {
for (const rp of customData) {
dataPaths.push(joinPath(extension.extensionUri, rp).toString());
}
}
}
return dataPaths;
}

View File

@ -0,0 +1,322 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import {
languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions,
Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend,
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands
} from 'vscode';
import {
LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams,
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient
} from 'vscode-languageclient';
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
import { activateTagClosing } from './tagClosing';
import { RequestService } from './requests';
import { getCustomDataSource } from './customData';
namespace CustomDataChangedNotification {
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
}
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
}
namespace OnTypeRenameRequest {
export const type: RequestType<TextDocumentPositionParams, LspRange[] | null, any, any> = new RequestType('html/onTypeRename');
}
// experimental: semantic tokens
interface SemanticTokenParams {
textDocument: TextDocumentIdentifier;
ranges?: LspRange[];
}
namespace SemanticTokenRequest {
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
}
namespace SemanticTokenLegendRequest {
export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any, any> = new RequestType0('html/semanticTokenLegend');
}
namespace SettingIds {
export const renameOnType = 'editor.renameOnType';
export const formatEnable = 'html.format.enable';
}
export interface TelemetryReporter {
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;
}, measurements?: {
[key: string]: number;
}): void;
}
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient;
export interface Runtime {
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } };
fs?: RequestService;
telemetry?: TelemetryReporter;
}
export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) {
let toDispose = context.subscriptions;
let documentSelector = ['html', 'handlebars'];
let embeddedLanguages = { css: true, javascript: true };
let rangeFormatting: Disposable | undefined = undefined;
const customDataSource = getCustomDataSource(context.subscriptions);
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector,
synchronize: {
configurationSection: ['html', 'css', 'javascript'], // the settings to synchronize
},
initializationOptions: {
embeddedLanguages,
handledSchemas: ['file'],
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting.
},
middleware: {
// testing the replace / insert mode
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
function updateRanges(item: CompletionItem) {
const range = item.range;
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
item.range = { inserting: new Range(range.start, position), replacing: range };
}
}
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
if (r) {
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
}
return r;
}
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
const r = next(document, position, context, token);
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
return r.then(updateProposals);
}
return updateProposals(r);
}
}
};
// Create the language client and start the client.
let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions);
client.registerProposedFeatures();
let disposable = client.start();
toDispose.push(disposable);
client.onReady().then(() => {
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
customDataSource.onDidChange(() => {
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
});
let tagRequestor = (document: TextDocument, position: Position) => {
let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
return client.sendRequest(TagCloseRequest.type, param);
};
disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags');
toDispose.push(disposable);
disposable = client.onTelemetry(e => {
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
});
toDispose.push(disposable);
// manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652.
updateFormatterRegistration();
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration()));
client.sendRequest(SemanticTokenLegendRequest.type).then(legend => {
if (legend) {
const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = {
provideDocumentSemanticTokens(doc) {
const params: SemanticTokenParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
};
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
return data && new SemanticTokens(new Uint32Array(data));
});
},
provideDocumentRangeSemanticTokens(doc, range) {
const params: SemanticTokenParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
ranges: [client.code2ProtocolConverter.asRange(range)]
};
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
return data && new SemanticTokens(new Uint32Array(data));
});
}
};
toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers)));
}
});
disposable = languages.registerOnTypeRenameProvider(documentSelector, {
async provideOnTypeRenameRanges(document, position) {
const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
return client.sendRequest(OnTypeRenameRequest.type, param).then(response => {
if (response) {
return {
ranges: response.map(r => client.protocol2CodeConverter.asRange(r))
};
}
return undefined;
});
}
});
toDispose.push(disposable);
});
function updateFormatterRegistration() {
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
if (!formatEnabled && rangeFormatting) {
rangeFormatting.dispose();
rangeFormatting = undefined;
} else if (formatEnabled && !rangeFormatting) {
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
let params: DocumentRangeFormattingParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
range: client.code2ProtocolConverter.asRange(range),
options: client.code2ProtocolConverter.asFormattingOptions(options)
};
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
client.protocol2CodeConverter.asTextEdits,
(error) => {
client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []);
return Promise.resolve([]);
}
);
}
});
}
}
languages.setLanguageConfiguration('html', {
indentationRules: {
increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/
},
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
action: { indentAction: IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: IndentAction.Indent }
}
],
});
languages.setLanguageConfiguration('handlebars', {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
action: { indentAction: IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: IndentAction.Indent }
}
],
});
const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/;
languages.registerCompletionItemProvider(documentSelector, {
provideCompletionItems(doc, pos) {
const results: CompletionItem[] = [];
let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
let match = lineUntilPos.match(regionCompletionRegExpr);
if (match) {
let range = new Range(new Position(pos.line, match[1].length), pos);
let beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
beginProposal.range = range;
beginProposal.insertText = new SnippetString('<!-- #region $1-->');
beginProposal.documentation = localize('folding.start', 'Folding Region Start');
beginProposal.filterText = match[2];
beginProposal.sortText = 'za';
results.push(beginProposal);
let endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
endProposal.range = range;
endProposal.insertText = new SnippetString('<!-- #endregion -->');
endProposal.documentation = localize('folding.end', 'Folding Region End');
endProposal.filterText = match[2];
endProposal.sortText = 'zb';
results.push(endProposal);
}
let match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr);
if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) {
let range = new Range(new Position(pos.line, match2[1].length), pos);
let snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet);
snippetProposal.range = range;
const content = ['<!DOCTYPE html>',
'<html>',
'<head>',
'\t<meta charset=\'utf-8\'>',
'\t<meta http-equiv=\'X-UA-Compatible\' content=\'IE=edge\'>',
'\t<title>${1:Page Title}</title>',
'\t<meta name=\'viewport\' content=\'width=device-width, initial-scale=1\'>',
'\t<link rel=\'stylesheet\' type=\'text/css\' media=\'screen\' href=\'${2:main.css}\'>',
'\t<script src=\'${3:main.js}\'></script>',
'</head>',
'<body>',
'\t$0',
'</body>',
'</html>'].join('\n');
snippetProposal.insertText = new SnippetString(content);
snippetProposal.documentation = localize('folding.html', 'Simple HTML5 starting point');
snippetProposal.filterText = match2[2];
snippetProposal.sortText = 'za';
results.push(snippetProposal);
}
return results;
}
});
const promptForTypeOnRenameKey = 'html.promptForTypeOnRename';
const promptForTypeOnRename = extensions.getExtension('formulahendry.auto-rename-tag') !== undefined &&
(context.globalState.get(promptForTypeOnRenameKey) !== false) &&
!workspace.getConfiguration('editor', { languageId: 'html' }).get('renameOnType');
if (promptForTypeOnRename) {
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
if (e && documentSelector.indexOf(e.document.languageId) !== -1) {
context.globalState.update(promptForTypeOnRenameKey, false);
activeEditorListener.dispose();
const configure = localize('configureButton', 'Configure');
const res = await window.showInformationMessage(localize('renameOnTypeQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
if (res === configure) {
commands.executeCommand('workbench.action.openSettings', SettingIds.renameOnType);
}
}
});
toDispose.push(activeEditorListener);
}
toDispose.push();
}

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 const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];

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.
*--------------------------------------------------------------------------------------------*/
import { getNodeFSRequestService } from './nodeFs';
import { ExtensionContext } from 'vscode';
import { startClient, LanguageClientConstructor } from '../htmlClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
import { TextDecoder } from 'util';
import * as fs from 'fs';
import TelemetryReporter from 'vscode-extension-telemetry';
let telemetry: TelemetryReporter | undefined;
// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
let clientPackageJSON = getPackageInfo(context);
telemetry = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey);
const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/htmlServerMain`;
const serverModule = context.asAbsolutePath(serverMain);
// The debug options for the server
const debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] };
// If the extension is launch in debug mode the debug server options are use
// Otherwise the run options are used
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, serverOptions, clientOptions);
};
startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder, telemetry });
}
interface IPackageInfo {
name: string;
version: string;
aiKey: string;
main: string;
}
function getPackageInfo(context: ExtensionContext): IPackageInfo {
const location = context.asAbsolutePath('./package.json');
try {
return JSON.parse(fs.readFileSync(location).toString());
} catch (e) {
console.log(`Problems reading ${location}: ${e}`);
return { name: '', version: '', aiKey: '', main: '' };
}
}

View File

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { Uri } from 'vscode';
import { getScheme, RequestService, FileType } from '../requests';
export function getNodeFSRequestService(): RequestService {
function ensureFileUri(location: string) {
if (getScheme(location) !== 'file') {
throw new Error('fileRequestService can only handle file URLs');
}
}
return {
getContent(location: string, encoding?: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.readFile(uri.fsPath, encoding, (err, buf) => {
if (err) {
return e(err);
}
c(buf.toString());
});
});
},
stat(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.stat(uri.fsPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
} else {
return e(err);
}
}
let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}
c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
},
readDirectory(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const path = Uri.parse(location).fsPath;
fs.readdir(path, { withFileTypes: true }, (err, children) => {
if (err) {
return e(err);
}
c(children.map(stat => {
if (stat.isSymbolicLink()) {
return [stat.name, FileType.SymbolicLink];
} else if (stat.isDirectory()) {
return [stat.name, FileType.Directory];
} else if (stat.isFile()) {
return [stat.name, FileType.File];
} else {
return [stat.name, FileType.Unknown];
}
}));
});
});
}
};
}

View File

@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, workspace } from 'vscode';
import { RequestType, CommonLanguageClient } from 'vscode-languageclient';
import { Runtime } from './htmlClient';
export namespace FsContentRequest {
export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content');
}
export namespace FsStatRequest {
export const type: RequestType<string, FileStat, any, any> = new RequestType('fs/stat');
}
export namespace FsReadDirRequest {
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
}
export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) {
client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => {
const uri = Uri.parse(param.uri);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.getContent(param.uri);
}
return workspace.fs.readFile(uri).then(buffer => {
return new runtime.TextDecoder(param.encoding).decode(buffer);
});
});
client.onRequest(FsReadDirRequest.type, (uriString: string) => {
const uri = Uri.parse(uriString);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.readDirectory(uriString);
}
return workspace.fs.readDirectory(uri);
});
client.onRequest(FsStatRequest.type, (uriString: string) => {
const uri = Uri.parse(uriString);
if (uri.scheme === 'file' && runtime.fs) {
return runtime.fs.stat(uriString);
}
return workspace.fs.stat(uri);
});
}
export enum FileType {
/**
* The file type is unknown.
*/
Unknown = 0,
/**
* A regular file.
*/
File = 1,
/**
* A directory.
*/
Directory = 2,
/**
* A symbolic link to a file.
*/
SymbolicLink = 64
}
export interface FileStat {
/**
* The type of the file, e.g. is a regular file, a directory, or symbolic link
* to a file.
*/
type: FileType;
/**
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
/**
* The size in bytes.
*/
size: number;
}
export interface RequestService {
getContent(uri: string, encoding?: string): Promise<string>;
stat(uri: string): Promise<FileStat>;
readDirectory(uri: string): Promise<[string, FileType][]>;
}
export function getScheme(uri: string) {
return uri.substr(0, uri.indexOf(':'));
}
export function dirname(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
}
export function basename(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return uri.substr(lastIndexOfSlash + 1);
}
const Slash = '/'.charCodeAt(0);
const Dot = '.'.charCodeAt(0);
export function isAbsolutePath(path: string) {
return path.charCodeAt(0) === Slash;
}
export function resolvePath(uri: Uri, path: string): Uri {
if (isAbsolutePath(path)) {
return uri.with({ path: normalizePath(path.split('/')) });
}
return joinPath(uri, path);
}
export function normalizePath(parts: string[]): string {
const newParts: string[] = [];
for (const part of parts) {
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
// ignore
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
newParts.pop();
} else {
newParts.push(part);
}
}
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
newParts.push('');
}
let res = newParts.join('/');
if (parts[0].length === 0) {
res = '/' + res;
}
return res;
}
export function joinPath(uri: Uri, ...paths: string[]): Uri {
const parts = uri.path.split('/');
for (let path of paths) {
parts.push(...path.split('/'));
}
return uri.with({ path: normalizePath(parts) });
}

View File

@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, workspace, Disposable, TextDocumentContentChangeEvent, TextDocument, Position, SnippetString } from 'vscode';
export function activateTagClosing(tagProvider: (document: TextDocument, position: Position) => Thenable<string>, supportedLanguages: { [id: string]: boolean }, configName: string): Disposable {
let disposables: Disposable[] = [];
workspace.onDidChangeTextDocument(event => onDidChangeTextDocument(event.document, event.contentChanges), null, disposables);
let isEnabled = false;
updateEnabledState();
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
let timeout: NodeJS.Timer | undefined = undefined;
function updateEnabledState() {
isEnabled = false;
let editor = window.activeTextEditor;
if (!editor) {
return;
}
let document = editor.document;
if (!supportedLanguages[document.languageId]) {
return;
}
if (!workspace.getConfiguration(undefined, document.uri).get<boolean>(configName)) {
return;
}
isEnabled = true;
}
function onDidChangeTextDocument(document: TextDocument, changes: readonly TextDocumentContentChangeEvent[]) {
if (!isEnabled) {
return;
}
let activeDocument = window.activeTextEditor && window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
if (typeof timeout !== 'undefined') {
clearTimeout(timeout);
}
let lastChange = changes[changes.length - 1];
let lastCharacter = lastChange.text[lastChange.text.length - 1];
if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') {
return;
}
let rangeStart = lastChange.range.start;
let version = document.version;
timeout = setTimeout(() => {
let position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
tagProvider(document, position).then(text => {
if (text && isEnabled) {
let activeEditor = window.activeTextEditor;
if (activeEditor) {
let activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
let selections = activeEditor.selections;
if (selections.length && selections.some(s => s.active.isEqual(position))) {
activeEditor.insertSnippet(new SnippetString(text), selections.map(s => s.active));
} else {
activeEditor.insertSnippet(new SnippetString(text), position);
}
}
}
}
});
timeout = undefined;
}, 100);
}
return Disposable.from(...disposables);
}

View File

@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../../src/vs/vscode.d.ts'/>
/// <reference path="../../../../../src/vs/vscode.proposed.d.ts" />

View File

@ -0,0 +1,9 @@
{
"extends": "../../shared.tsconfig.json",
"compilerOptions": {
"outDir": "./out"
},
"include": [
"src/**/*"
]
}

View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withBrowserDefaults = require('../shared.webpack.config').browser;
const path = require('path');
module.exports = withBrowserDefaults({
context: path.join(__dirname, 'client'),
entry: {
extension: './src/browser/htmlClientMain.ts'
},
output: {
filename: 'htmlClientMain.js',
path: path.join(__dirname, 'client', 'dist', 'browser')
}
});

View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
const path = require('path');
module.exports = withDefaults({
context: path.join(__dirname, 'client'),
entry: {
extension: './src/node/htmlClientMain.ts',
},
output: {
filename: 'htmlClientMain.js',
path: path.join(__dirname, 'client', 'dist', 'node')
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,212 @@
{
"enableProposedApi": true,
"name": "html-language-features",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.0",
"publisher": "vscode",
"license": "MIT",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"engines": {
"vscode": "0.10.x"
},
"icon": "icons/html.png",
"activationEvents": [
"onLanguage:html",
"onLanguage:handlebars"
],
"main": "./client/out/node/htmlClientMain",
"browser": "./client/dist/browser/htmlClientMain",
"scripts": {
"compile": "npx gulp compile-extension:html-language-features-client compile-extension:html-language-features-server",
"watch": "npx gulp watch-extension:html-language-features-client watch-extension:html-language-features-server",
"postinstall": "cd server && yarn install",
"install-client-next": "yarn add vscode-languageclient@next"
},
"categories": [
"Programming Languages"
],
"contributes": {
"configuration": {
"id": "html",
"order": 20,
"type": "object",
"title": "HTML",
"properties": {
"html.customData": {
"type": "array",
"markdownDescription": "%html.customData.desc%",
"default": [],
"items": {
"type": "string"
},
"scope": "resource"
},
"html.format.enable": {
"type": "boolean",
"scope": "window",
"default": true,
"description": "%html.format.enable.desc%"
},
"html.format.wrapLineLength": {
"type": "integer",
"scope": "resource",
"default": 120,
"description": "%html.format.wrapLineLength.desc%"
},
"html.format.unformatted": {
"type": [
"string",
"null"
],
"scope": "resource",
"default": "wbr",
"markdownDescription": "%html.format.unformatted.desc%"
},
"html.format.contentUnformatted": {
"type": [
"string",
"null"
],
"scope": "resource",
"default": "pre,code,textarea",
"markdownDescription": "%html.format.contentUnformatted.desc%"
},
"html.format.indentInnerHtml": {
"type": "boolean",
"scope": "resource",
"default": false,
"markdownDescription": "%html.format.indentInnerHtml.desc%"
},
"html.format.preserveNewLines": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%html.format.preserveNewLines.desc%"
},
"html.format.maxPreserveNewLines": {
"type": [
"number",
"null"
],
"scope": "resource",
"default": null,
"markdownDescription": "%html.format.maxPreserveNewLines.desc%"
},
"html.format.indentHandlebars": {
"type": "boolean",
"scope": "resource",
"default": false,
"markdownDescription": "%html.format.indentHandlebars.desc%"
},
"html.format.endWithNewline": {
"type": "boolean",
"scope": "resource",
"default": false,
"description": "%html.format.endWithNewline.desc%"
},
"html.format.extraLiners": {
"type": [
"string",
"null"
],
"scope": "resource",
"default": "head, body, /html",
"markdownDescription": "%html.format.extraLiners.desc%"
},
"html.format.wrapAttributes": {
"type": "string",
"scope": "resource",
"default": "auto",
"enum": [
"auto",
"force",
"force-aligned",
"force-expand-multiline",
"aligned-multiple",
"preserve",
"preserve-aligned"
],
"enumDescriptions": [
"%html.format.wrapAttributes.auto%",
"%html.format.wrapAttributes.force%",
"%html.format.wrapAttributes.forcealign%",
"%html.format.wrapAttributes.forcemultiline%",
"%html.format.wrapAttributes.alignedmultiple%",
"%html.format.wrapAttributes.preserve%",
"%html.format.wrapAttributes.preservealigned%"
],
"description": "%html.format.wrapAttributes.desc%"
},
"html.suggest.html5": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%html.suggest.html5.desc%"
},
"html.validate.scripts": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%html.validate.scripts%"
},
"html.validate.styles": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%html.validate.styles%"
},
"html.autoClosingTags": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%html.autoClosingTags%"
},
"html.mirrorCursorOnMatchingTag": {
"type": "boolean",
"scope": "resource",
"default": false,
"description": "%html.mirrorCursorOnMatchingTag%",
"deprecationMessage": "%html.mirrorCursorOnMatchingTagDeprecationMessage%"
},
"html.trace.server": {
"type": "string",
"scope": "window",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "%html.trace.server.desc%"
}
}
},
"configurationDefaults": {
"[html]": {
"editor.suggest.insertMode": "replace"
},
"[handlebars]": {
"editor.suggest.insertMode": "replace"
}
},
"jsonValidation": [
{
"fileMatch": "*.html-data.json",
"url": "https://raw.githubusercontent.com/microsoft/vscode-html-languageservice/master/docs/customData.schema.json"
},
{
"fileMatch": "package.json",
"url": "./schemas/package.schema.json"
}
]
},
"dependencies": {
"vscode-extension-telemetry": "0.1.1",
"vscode-languageclient": "7.0.0-next.5.1",
"vscode-nls": "^4.1.2"
},
"devDependencies": {
"@types/node": "^12.11.7"
}
}

View File

@ -0,0 +1,30 @@
{
"displayName": "HTML Language Features",
"description": "Provides rich language support for HTML and Handlebar files",
"html.customData.desc": "A list of relative file paths pointing to JSON files following the [custom data format](https://github.com/microsoft/vscode-html-languageservice/blob/master/docs/customData.md).\n\nVS Code loads custom data on startup to enhance its HTML support for the custom HTML tags, attributes and attribute values you specify in the JSON files.\n\nThe file paths are relative to workspace and only workspace folder settings are considered.",
"html.format.enable.desc": "Enable/disable default HTML formatter.",
"html.format.wrapLineLength.desc": "Maximum amount of characters per line (0 = disable).",
"html.format.unformatted.desc": "List of tags, comma separated, that shouldn't be reformatted. `null` defaults to all tags listed at https://www.w3.org/TR/html5/dom.html#phrasing-content.",
"html.format.contentUnformatted.desc": "List of tags, comma separated, where the content shouldn't be reformatted. `null` defaults to the `pre` tag.",
"html.format.indentInnerHtml.desc": "Indent `<head>` and `<body>` sections.",
"html.format.preserveNewLines.desc": "Controls whether existing line breaks before elements should be preserved. Only works before elements, not inside tags or for text.",
"html.format.maxPreserveNewLines.desc": "Maximum number of line breaks to be preserved in one chunk. Use `null` for unlimited.",
"html.format.indentHandlebars.desc": "Format and indent `{{#foo}}` and `{{/foo}}`.",
"html.format.endWithNewline.desc": "End with a newline.",
"html.format.extraLiners.desc": "List of tags, comma separated, that should have an extra newline before them. `null` defaults to `\"head, body, /html\"`.",
"html.format.wrapAttributes.desc": "Wrap attributes.",
"html.format.wrapAttributes.auto": "Wrap attributes only when line length is exceeded.",
"html.format.wrapAttributes.force": "Wrap each attribute except first.",
"html.format.wrapAttributes.forcealign": "Wrap each attribute except first and keep aligned.",
"html.format.wrapAttributes.forcemultiline": "Wrap each attribute.",
"html.format.wrapAttributes.alignedmultiple": "Wrap when line length is exceeded, align attributes vertically.",
"html.format.wrapAttributes.preserve": "Preserve wrapping of attributes",
"html.format.wrapAttributes.preservealigned": "Preserve wrapping of attributes but align.",
"html.suggest.html5.desc": "Controls whether the built-in HTML language support suggests HTML5 tags, properties and values.",
"html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.",
"html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.",
"html.validate.styles": "Controls whether the built-in HTML language support validates embedded styles.",
"html.autoClosingTags": "Enable/disable autoclosing of HTML tags.",
"html.mirrorCursorOnMatchingTag": "Enable/disable mirroring cursor on matching HTML tag.",
"html.mirrorCursorOnMatchingTagDeprecationMessage": "Deprecated in favor of `editor.renameOnType`"
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "HTML contributions to package.json",
"type": "object",
"properties": {
"contributes": {
"type": "object",
"properties": {
"html.customData": {
"type": "array",
"markdownDescription": "A list of relative file paths pointing to JSON files following the [custom data format](https://github.com/microsoft/vscode-html-languageservice/blob/master/docs/customData.md).\n\nVS Code loads custom data on startup to enhance its HTML support for the custom HTML tags, attributes and attribute values you specify in the JSON files.\n\nThe file paths are relative to workspace and only workspace folder settings are considered.",
"items": {
"type": "string",
"description": "Relative path to a HTML custom data file"
}
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
"configurations": [
{
"name": "Attach",
"type": "node",
"request": "attach",
"port": 6045,
"protocol": "inspector",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
},
{
"name": "Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/../../../node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [
"--timeout",
"999999",
"--colors"
],
"cwd": "${workspaceFolder}",
"runtimeExecutable": null,
"runtimeArgs": [],
"env": {},
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}
]
}

View File

@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm watch",
"command": "npm",
"args": ["run", "watch"],
"type": "shell",
"presentation": {
"reveal": "silent",
"focus": false,
"panel": "shared"
},
"isBackground": true,
"problemMatcher": "$tsc-watch"
}
],
}

View File

@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// a webpack loader that bundles all library definitions (d.ts) for the embedded JavaScript engine.
const path = require('path');
const fs = require('fs');
const TYPESCRIPT_LIB_SOURCE = path.join(__dirname, '../../../node_modules/typescript/lib');
const JQUERY_DTS = path.join(__dirname, '../lib/jquery.d.ts');
module.exports = function () {
function getFileName(name) {
return (name === '' ? 'lib.d.ts' : `lib.${name}.d.ts`);
}
function readLibFile(name) {
var srcPath = path.join(TYPESCRIPT_LIB_SOURCE, getFileName(name));
return fs.readFileSync(srcPath).toString();
}
var queue = [];
var in_queue = {};
var enqueue = function (name) {
if (in_queue[name]) {
return;
}
in_queue[name] = true;
queue.push(name);
};
enqueue('es6');
var result = [];
while (queue.length > 0) {
var name = queue.shift();
var contents = readLibFile(name);
var lines = contents.split(/\r\n|\r|\n/);
var outputLines = [];
for (let i = 0; i < lines.length; i++) {
let m = lines[i].match(/\/\/\/\s*<reference\s*lib="([^"]+)"/);
if (m) {
enqueue(m[1]);
}
outputLines.push(lines[i]);
}
result.push({
name: getFileName(name),
output: `"${escapeText(outputLines.join('\n'))}"`
});
}
const jquerySource = fs.readFileSync(JQUERY_DTS).toString();
var lines = jquerySource.split(/\r\n|\r|\n/);
result.push({
name: 'jquery',
output: `"${escapeText(lines.join('\n'))}"`
});
strResult = `\nconst libs : { [name:string]: string; } = {\n`
for (let i = result.length - 1; i >= 0; i--) {
strResult += `"${result[i].name}": ${result[i].output},\n`;
}
strResult += `\n};`
strResult += `export function loadLibrary(name: string) : string {\n return libs[name] || ''; \n}`;
return strResult;
}
/**
* Escape text such that it can be used in a javascript string enclosed by double quotes (")
*/
function escapeText(text) {
// See http://www.javascriptkit.com/jsref/escapesequence.shtml
var _backspace = '\b'.charCodeAt(0);
var _formFeed = '\f'.charCodeAt(0);
var _newLine = '\n'.charCodeAt(0);
var _nullChar = 0;
var _carriageReturn = '\r'.charCodeAt(0);
var _tab = '\t'.charCodeAt(0);
var _verticalTab = '\v'.charCodeAt(0);
var _backslash = '\\'.charCodeAt(0);
var _doubleQuote = '"'.charCodeAt(0);
var startPos = 0, chrCode, replaceWith = null, resultPieces = [];
for (var i = 0, len = text.length; i < len; i++) {
chrCode = text.charCodeAt(i);
switch (chrCode) {
case _backspace:
replaceWith = '\\b';
break;
case _formFeed:
replaceWith = '\\f';
break;
case _newLine:
replaceWith = '\\n';
break;
case _nullChar:
replaceWith = '\\0';
break;
case _carriageReturn:
replaceWith = '\\r';
break;
case _tab:
replaceWith = '\\t';
break;
case _verticalTab:
replaceWith = '\\v';
break;
case _backslash:
replaceWith = '\\\\';
break;
case _doubleQuote:
replaceWith = '\\"';
break;
}
if (replaceWith !== null) {
resultPieces.push(text.substring(startPos, i));
resultPieces.push(replaceWith);
startPos = i + 1;
replaceWith = null;
}
}
resultPieces.push(text.substring(startPos, len));
return resultPieces.join('');
}

View File

@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withBrowserDefaults = require('../../shared.webpack.config').browser;
const path = require('path');
const serverConfig = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/browser/htmlServerMain.ts',
},
output: {
filename: 'htmlServerMain.js',
path: path.join(__dirname, 'dist', 'browser'),
libraryTarget: 'var'
},
optimization: {
splitChunks: {
chunks: 'async'
}
}
});
serverConfig.module.noParse = /typescript[\/\\]lib[\/\\]typescript\.js/;
serverConfig.module.rules.push({
test: /javascriptLibs.ts$/,
use: [
{
loader: path.resolve(__dirname, 'build', 'javaScriptLibraryLoader.js')
}
]
});
module.exports = serverConfig;

View File

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../../shared.webpack.config');
const path = require('path');
module.exports = withDefaults({
context: path.join(__dirname),
entry: {
extension: './src/node/htmlServerMain.ts',
},
output: {
filename: 'htmlServerMain.js',
path: path.join(__dirname, 'dist', 'node'),
},
externals: {
'typescript': 'commonjs typescript'
}
});

View File

@ -0,0 +1,16 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "definitelytyped",
"repositoryUrl": "https://github.com/DefinitelyTyped/DefinitelyTyped",
"commitHash": "69e3ac6bec3008271f76bbfa7cf69aa9198c4ff0"
}
},
"license": "MIT"
}
],
"version": 1
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "vscode-html-languageserver",
"description": "HTML language server",
"version": "1.0.0",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
"node": "*"
},
"main": "./out/node/htmlServerMain",
"dependencies": {
"vscode-css-languageservice": "^4.3.5",
"vscode-html-languageservice": "^3.1.4",
"vscode-languageserver": "7.0.0-next.3",
"vscode-nls": "^5.0.0",
"vscode-uri": "^2.1.2"
},
"devDependencies": {
"@types/mocha": "^8.0.3",
"@types/node": "^12.11.7",
"glob": "^7.1.6",
"mocha": "^8.1.3",
"mocha-junit-reporter": "^2.0.0",
"mocha-multi-reporters": "^1.1.7"
},
"scripts": {
"compile": "npx gulp compile-extension:html-language-features-server",
"watch": "npx gulp watch-extension:html-language-features-server",
"install-service-next": "yarn add vscode-css-languageservice@next && yarn add vscode-html-languageservice@next",
"install-service-local": "npm install ../../../../vscode-css-languageservice -f && npm install ../../../../vscode-html-languageservice -f",
"install-server-next": "yarn add vscode-languageserver@next",
"install-server-local": "npm install ../../../../vscode-languageserver-node/server -f",
"test": "npm run compile && node ./test/index.js"
}
}

View File

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser';
import { startServer } from '../htmlServer';
declare let self: any;
const messageReader = new BrowserMessageReader(self);
const messageWriter = new BrowserMessageWriter(self);
const connection = createConnection(messageReader, messageWriter);
startServer(connection, {});

View File

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice';
import { RequestService } from './requests';
export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise<IHTMLDataProvider[]> {
const providers = dataPaths.map(async p => {
try {
const content = await requestService.getContent(p);
return parseHTMLData(p, content);
} catch (e) {
return newHTMLDataProvider(p, { version: 1 });
}
});
return Promise.all(providers);
}
function parseHTMLData(id: string, source: string): IHTMLDataProvider {
let rawData: any;
try {
rawData = JSON.parse(source);
} catch (err) {
return newHTMLDataProvider(id, { version: 1 });
}
return newHTMLDataProvider(id, {
version: rawData.version || 1,
tags: rawData.tags || [],
globalAttributes: rawData.globalAttributes || [],
valueSets: rawData.valueSets || []
});
}

View File

@ -0,0 +1,559 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Connection, TextDocuments, InitializeParams, InitializeResult, RequestType,
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType
} from 'vscode-languageserver';
import {
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
} from './modes/languageModes';
import { format } from './modes/formatting';
import { pushAll } from './utils/arrays';
import { getDocumentContext } from './utils/documentContext';
import { URI } from 'vscode-uri';
import { formatError, runSafe } from './utils/runner';
import { getFoldingRanges } from './modes/htmlFolding';
import { fetchHTMLDataProviders } from './customData';
import { getSelectionRanges } from './modes/selectionRanges';
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
import { RequestService, getRequestService } from './requests';
namespace CustomDataChangedNotification {
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
}
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
}
namespace OnTypeRenameRequest {
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
}
// experimental: semantic tokens
interface SemanticTokenParams {
textDocument: TextDocumentIdentifier;
ranges?: Range[];
}
namespace SemanticTokenRequest {
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
}
namespace SemanticTokenLegendRequest {
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
}
export interface RuntimeEnvironment {
file?: RequestService;
http?: RequestService
configureHttpRequests?(proxy: string, strictSSL: boolean): void;
}
export function startServer(connection: Connection, runtime: RuntimeEnvironment) {
// Create a text document manager.
const documents = new TextDocuments(TextDocument);
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
let workspaceFolders: WorkspaceFolder[] = [];
let languageModes: LanguageModes;
let clientSnippetSupport = false;
let dynamicFormatterRegistration = false;
let scopedSettingsSupport = false;
let workspaceFoldersSupport = false;
let foldingRangeLimit = Number.MAX_VALUE;
const notReady = () => Promise.reject('Not Ready');
let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady };
let globalSettings: Settings = {};
let documentSettings: { [key: string]: Thenable<Settings> } = {};
// remove document settings on close
documents.onDidClose(e => {
delete documentSettings[e.document.uri];
});
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
if (scopedSettingsSupport && needsDocumentSettings()) {
let promise = documentSettings[textDocument.uri];
if (!promise) {
const scopeUri = textDocument.uri;
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
documentSettings[textDocument.uri] = promise;
}
return promise;
}
return Promise.resolve(undefined);
}
// After the server has started the client sends an initialize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilities
connection.onInitialize((params: InitializeParams): InitializeResult => {
const initializationOptions = params.initializationOptions;
workspaceFolders = (<any>params).workspaceFolders;
if (!Array.isArray(workspaceFolders)) {
workspaceFolders = [];
if (params.rootPath) {
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
}
}
requestService = getRequestService(initializationOptions?.handledSchemas || ['file'], connection, runtime);
const workspace = {
get settings() { return globalSettings; },
get folders() { return workspaceFolders; }
};
languageModes = getLanguageModes(initializationOptions?.embeddedLanguages || { css: true, javascript: true }, workspace, params.capabilities, requestService);
const dataPaths: string[] = initializationOptions?.dataPaths || [];
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
languageModes.updateDataProviders(dataProviders);
});
documents.onDidClose(e => {
languageModes.onDocumentRemoved(e.document);
});
connection.onShutdown(() => {
languageModes.dispose();
});
function getClientCapability<T>(name: string, def: T) {
const keys = name.split('.');
let c: any = params.capabilities;
for (let i = 0; c && i < keys.length; i++) {
if (!c.hasOwnProperty(keys[i])) {
return def;
}
c = c[keys[i]];
}
return c;
}
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions?.provideFormatter !== 'boolean');
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
const capabilities: ServerCapabilities = {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
hoverProvider: true,
documentHighlightProvider: true,
documentRangeFormattingProvider: initializationOptions?.provideFormatter === true,
documentLinkProvider: { resolveProvider: false },
documentSymbolProvider: true,
definitionProvider: true,
signatureHelpProvider: { triggerCharacters: ['('] },
referencesProvider: true,
colorProvider: {},
foldingRangeProvider: true,
selectionRangeProvider: true,
renameProvider: true
};
return { capabilities };
});
connection.onInitialized(() => {
if (workspaceFoldersSupport) {
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
const toAdd = e.event.added;
const toRemove = e.event.removed;
const updatedFolders = [];
if (workspaceFolders) {
for (const folder of workspaceFolders) {
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
updatedFolders.push(folder);
}
}
}
workspaceFolders = updatedFolders.concat(toAdd);
documents.all().forEach(triggerValidation);
});
}
});
let formatterRegistration: Thenable<Disposable> | null = null;
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
globalSettings = change.settings;
documentSettings = {}; // reset all document settings
documents.all().forEach(triggerValidation);
// dynamically enable & disable the formatter
if (dynamicFormatterRegistration) {
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
if (enableFormatter) {
if (!formatterRegistration) {
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
}
} else if (formatterRegistration) {
formatterRegistration.then(r => r.dispose());
formatterRegistration = null;
}
}
});
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
const validationDelayMs = 500;
// 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: [] });
});
function cleanPendingValidation(textDocument: TextDocument): void {
const 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 isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
const validationSettings = settings && settings.html && settings.html.validate;
if (validationSettings) {
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
}
return true;
}
async function validateTextDocument(textDocument: TextDocument) {
try {
const version = textDocument.version;
const diagnostics: Diagnostic[] = [];
if (textDocument.languageId === 'html') {
const modes = languageModes.getAllModesInDocument(textDocument);
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
const latestTextDocument = documents.get(textDocument.uri);
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
for (const mode of modes) {
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
pushAll(diagnostics, await mode.doValidation(latestTextDocument, settings));
}
}
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
}
}
} catch (e) {
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
}
}
connection.onCompletion(async (textDocumentPosition, token) => {
return runSafe(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (!mode || !mode.doComplete) {
return { isIncomplete: true, items: [] };
}
const doComplete = mode.doComplete!;
if (mode.getId() !== 'html') {
/* __GDPR__
"html.embbedded.complete" : {
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
}
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
const documentContext = getDocumentContext(document.uri, workspaceFolders);
return doComplete(document, textDocumentPosition.position, documentContext, settings);
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onCompletionResolve((item, token) => {
return runSafe(async () => {
const data = item.data;
if (data && data.languageId && data.uri) {
const mode = languageModes.getMode(data.languageId);
const document = documents.get(data.uri);
if (mode && mode.doResolve && document) {
return mode.doResolve(document, item);
}
}
return item;
}, item, `Error while resolving completion proposal`, token);
});
connection.onHover((textDocumentPosition, token) => {
return runSafe(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (mode && mode.doHover) {
return mode.doHover(document, textDocumentPosition.position);
}
}
return null;
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onDocumentHighlight((documentHighlightParams, token) => {
return runSafe(async () => {
const document = documents.get(documentHighlightParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
if (mode && mode.findDocumentHighlight) {
return mode.findDocumentHighlight(document, documentHighlightParams.position);
}
}
return [];
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
});
connection.onDefinition((definitionParams, token) => {
return runSafe(async () => {
const document = documents.get(definitionParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
if (mode && mode.findDefinition) {
return mode.findDefinition(document, definitionParams.position);
}
}
return [];
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
});
connection.onReferences((referenceParams, token) => {
return runSafe(async () => {
const document = documents.get(referenceParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
if (mode && mode.findReferences) {
return mode.findReferences(document, referenceParams.position);
}
}
return [];
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
});
connection.onSignatureHelp((signatureHelpParms, token) => {
return runSafe(async () => {
const document = documents.get(signatureHelpParms.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
if (mode && mode.doSignatureHelp) {
return mode.doSignatureHelp(document, signatureHelpParms.position);
}
}
return null;
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
});
connection.onDocumentRangeFormatting(async (formatParams, token) => {
return runSafe(async () => {
const document = documents.get(formatParams.textDocument.uri);
if (document) {
let settings = await getDocumentSettings(document, () => true);
if (!settings) {
settings = globalSettings;
}
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
}
return [];
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
});
connection.onDocumentLinks((documentLinkParam, token) => {
return runSafe(async () => {
const document = documents.get(documentLinkParam.textDocument.uri);
const links: DocumentLink[] = [];
if (document) {
const documentContext = getDocumentContext(document.uri, workspaceFolders);
for (const m of languageModes.getAllModesInDocument(document)) {
if (m.findDocumentLinks) {
pushAll(links, await m.findDocumentLinks(document, documentContext));
}
}
}
return links;
}, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token);
});
connection.onDocumentSymbol((documentSymbolParms, token) => {
return runSafe(async () => {
const document = documents.get(documentSymbolParms.textDocument.uri);
const symbols: SymbolInformation[] = [];
if (document) {
for (const m of languageModes.getAllModesInDocument(document)) {
if (m.findDocumentSymbols) {
pushAll(symbols, await m.findDocumentSymbols(document));
}
}
}
return symbols;
}, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token);
});
connection.onRequest(DocumentColorRequest.type, (params, token) => {
return runSafe(async () => {
const infos: ColorInformation[] = [];
const document = documents.get(params.textDocument.uri);
if (document) {
for (const m of languageModes.getAllModesInDocument(document)) {
if (m.findDocumentColors) {
pushAll(infos, await m.findDocumentColors(document));
}
}
}
return infos;
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
});
connection.onRequest(ColorPresentationRequest.type, (params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, params.range.start);
if (mode && mode.getColorPresentations) {
return mode.getColorPresentations(document, params.color, params.range);
}
}
return [];
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
});
connection.onRequest(TagCloseRequest.type, (params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doAutoClose) {
return mode.doAutoClose(document, pos);
}
}
}
return null;
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
});
connection.onFoldingRanges((params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
}
return null;
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
});
connection.onSelectionRanges((params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSelectionRanges(languageModes, document, params.positions);
}
return [];
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
});
connection.onRenameRequest((params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
const position: Position = params.position;
if (document) {
const htmlMode = languageModes.getMode('html');
if (htmlMode && htmlMode.doRename) {
return htmlMode.doRename(document, position, params.newName);
}
}
return null;
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
});
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doOnTypeRename) {
return mode.doOnTypeRename(document, pos);
}
}
}
return null;
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
});
let semanticTokensProvider: SemanticTokenProvider | undefined;
function getSemanticTokenProvider() {
if (!semanticTokensProvider) {
semanticTokensProvider = newSemanticTokenProvider(languageModes);
}
return semanticTokensProvider;
}
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
return runSafe(async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
}
return null;
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
});
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
return runSafe(async () => {
return getSemanticTokenProvider().legend;
}, null, `Error while computing semantic tokens legend`, token);
});
connection.onNotification(CustomDataChangedNotification.type, dataPaths => {
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
languageModes.updateDataProviders(dataProviders);
});
});
// Listen on the connection
connection.listen();
}

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.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-html-languageservice';
export interface LanguageModelCache<T> {
get(document: TextDocument): T;
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}
export function getLanguageModelCache<T>(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache<T> {
let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {};
let nModels = 0;
let cleanupInterval: NodeJS.Timer | undefined = undefined;
if (cleanupIntervalTimeInSec > 0) {
cleanupInterval = setInterval(() => {
const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
const uris = Object.keys(languageModels);
for (const uri of uris) {
const languageModelInfo = languageModels[uri];
if (languageModelInfo.cTime < cutoffTime) {
delete languageModels[uri];
nModels--;
}
}
}, cleanupIntervalTimeInSec * 1000);
}
return {
get(document: TextDocument): T {
const version = document.version;
const languageId = document.languageId;
const languageModelInfo = languageModels[document.uri];
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
languageModelInfo.cTime = Date.now();
return languageModelInfo.languageModel;
}
const languageModel = parse(document);
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() };
if (!languageModelInfo) {
nModels++;
}
if (nModels === maxEntries) {
let oldestTime = Number.MAX_VALUE;
let oldestUri = null;
for (const uri in languageModels) {
const languageModelInfo = languageModels[uri];
if (languageModelInfo.cTime < oldestTime) {
oldestUri = uri;
oldestTime = languageModelInfo.cTime;
}
}
if (oldestUri) {
delete languageModels[oldestUri];
nModels--;
}
}
return languageModel;
},
onDocumentRemoved(document: TextDocument) {
const uri = document.uri;
if (languageModels[uri]) {
delete languageModels[uri];
nModels--;
}
},
dispose() {
if (typeof cleanupInterval !== 'undefined') {
clearInterval(cleanupInterval);
cleanupInterval = undefined;
languageModels = {};
nModels = 0;
}
}
};
}

View File

@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes';
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspace: Workspace): LanguageMode {
let embeddedCSSDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css'));
let cssStylesheets = getLanguageModelCache<Stylesheet>(10, 60, document => cssLanguageService.parseStylesheet(document));
return {
getId() {
return 'css';
},
async doValidation(document: TextDocument, settings = workspace.settings) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css);
},
async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) {
let embedded = embeddedCSSDocuments.get(document);
const stylesheet = cssStylesheets.get(embedded);
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create();
},
async doHover(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded));
},
async findDocumentHighlight(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded));
},
async findDocumentSymbols(document: TextDocument) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.findDocumentSymbols(embedded, cssStylesheets.get(embedded)).filter(s => s.name !== CSS_STYLE_RULE);
},
async findDefinition(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded));
},
async findReferences(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded));
},
async findDocumentColors(document: TextDocument) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.findDocumentColors(embedded, cssStylesheets.get(embedded));
},
async getColorPresentations(document: TextDocument, color: Color, range: Range) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.getColorPresentations(embedded, cssStylesheets.get(embedded), color, range);
},
async getFoldingRanges(document: TextDocument) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.getFoldingRanges(embedded, {});
},
async getSelectionRange(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.getSelectionRanges(embedded, [position], cssStylesheets.get(embedded))[0];
},
onDocumentRemoved(document: TextDocument) {
embeddedCSSDocuments.onDocumentRemoved(document);
cssStylesheets.onDocumentRemoved(document);
},
dispose() {
embeddedCSSDocuments.dispose();
cssStylesheets.dispose();
}
};
}

View File

@ -0,0 +1,233 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, Position, LanguageService, TokenType, Range } from './languageModes';
export interface LanguageRange extends Range {
languageId: string | undefined;
attributeValue?: boolean;
}
export interface HTMLDocumentRegions {
getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
getLanguageRanges(range: Range): LanguageRange[];
getLanguageAtPosition(position: Position): string | undefined;
getLanguagesInDocument(): string[];
getImportedScripts(): string[];
}
export const CSS_STYLE_RULE = '__';
interface EmbeddedRegion { languageId: string | undefined; start: number; end: number; attributeValue?: boolean; }
export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions {
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(document.getText());
let lastTagName: string = '';
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = 'javascript';
break;
case TokenType.Styles:
regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
break;
case TokenType.Script:
regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
let value = scanner.getTokenText();
if (value[0] === '\'' || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) {
languageIdFromType = 'javascript';
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = 'typescript';
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = document.getText()[start];
if (firstChar === '\'' || firstChar === '"') {
start++;
end--;
}
regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true });
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
return {
getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) => getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position),
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
getImportedScripts: () => importedScripts
};
}
function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] {
let result: LanguageRange[] = [];
let currentPos = range ? range.start : Position.create(0, 0);
let currentOffset = range ? document.offsetAt(range.start) : 0;
let endOffset = range ? document.offsetAt(range.end) : document.getText().length;
for (let region of regions) {
if (region.end > currentOffset && region.start < endOffset) {
let start = Math.max(region.start, currentOffset);
let startPos = document.positionAt(start);
if (currentOffset < region.start) {
result.push({
start: currentPos,
end: startPos,
languageId: 'html'
});
}
let end = Math.min(region.end, endOffset);
let endPos = document.positionAt(end);
if (end > region.start) {
result.push({
start: startPos,
end: endPos,
languageId: region.languageId,
attributeValue: region.attributeValue
});
}
currentOffset = end;
currentPos = endPos;
}
}
if (currentOffset < endOffset) {
let endPos = range ? range.end : document.positionAt(endOffset);
result.push({
start: currentPos,
end: endPos,
languageId: 'html'
});
}
return result;
}
function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] {
let result = [];
for (let region of regions) {
if (region.languageId && result.indexOf(region.languageId) === -1) {
result.push(region.languageId);
if (result.length === 3) {
return result;
}
}
}
result.push('html');
return result;
}
function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
let offset = document.offsetAt(position);
for (let region of regions) {
if (region.start <= offset) {
if (offset <= region.end) {
return region.languageId;
}
} else {
break;
}
}
return 'html';
}
function getEmbeddedDocument(document: TextDocument, contents: EmbeddedRegion[], languageId: string, ignoreAttributeValues: boolean): TextDocument {
let currentPos = 0;
let oldContent = document.getText();
let result = '';
let lastSuffix = '';
for (let c of contents) {
if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c));
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
lastSuffix = getSuffix(c);
}
}
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, '');
return TextDocument.create(document.uri, languageId, document.version, result);
}
function getPrefix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case 'css': return CSS_STYLE_RULE + '{';
}
}
return '';
}
function getSuffix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case 'css': return '}';
case 'javascript': return ';';
}
}
return '';
}
function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
let accumulatedWS = 0;
result += before;
for (let i = start + before.length; 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 - after.length);
result += after;
return result;
}
function append(result: string, str: string, n: number): string {
while (n > 0) {
if (n & 1) {
result += str;
}
n >>= 1;
str += str;
}
return result;
}
function getAttributeLanguage(attributeName: string): string | null {
let match = attributeName.match(/^(style)$|^(on\w+)$/i);
if (!match) {
return null;
}
return match[1] ? 'css' : 'javascript';
}

View File

@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModes, Settings, LanguageModeRange, TextDocument, Range, TextEdit, FormattingOptions, Position } from './languageModes';
import { pushAll } from '../utils/arrays';
import { isEOL } from '../utils/strings';
export async function format(languageModes: LanguageModes, document: TextDocument, formatRange: Range, formattingOptions: FormattingOptions, settings: Settings | undefined, enabledModes: { [mode: string]: boolean }) {
let result: TextEdit[] = [];
let endPos = formatRange.end;
let endOffset = document.offsetAt(endPos);
let content = document.getText();
if (endPos.character === 0 && endPos.line > 0 && endOffset !== content.length) {
// if selection ends after a new line, exclude that new line
let prevLineStart = document.offsetAt(Position.create(endPos.line - 1, 0));
while (isEOL(content, endOffset - 1) && endOffset > prevLineStart) {
endOffset--;
}
formatRange = Range.create(formatRange.start, document.positionAt(endOffset));
}
// run the html formatter on the full range and pass the result content to the embedded formatters.
// from the final content create a single edit
// advantages of this approach are
// - correct indents in the html document
// - correct initial indent for embedded formatters
// - no worrying of overlapping edits
// make sure we start in html
let allRanges = languageModes.getModesInRange(document, formatRange);
let i = 0;
let startPos = formatRange.start;
let isHTML = (range: LanguageModeRange) => range.mode && range.mode.getId() === 'html';
while (i < allRanges.length && !isHTML(allRanges[i])) {
let range = allRanges[i];
if (!range.attributeValue && range.mode && range.mode.format) {
let edits = await range.mode.format(document, Range.create(startPos, range.end), formattingOptions, settings);
pushAll(result, edits);
}
startPos = range.end;
i++;
}
if (i === allRanges.length) {
return result;
}
// modify the range
formatRange = Range.create(startPos, formatRange.end);
// perform a html format and apply changes to a new document
let htmlMode = languageModes.getMode('html')!;
let htmlEdits = await htmlMode.format!(document, formatRange, formattingOptions, settings);
let htmlFormattedContent = TextDocument.applyEdits(document, htmlEdits);
let newDocument = TextDocument.create(document.uri + '.tmp', document.languageId, document.version, htmlFormattedContent);
try {
// run embedded formatters on html formatted content: - formatters see correct initial indent
let afterFormatRangeLength = document.getText().length - document.offsetAt(formatRange.end); // length of unchanged content after replace range
let newFormatRange = Range.create(formatRange.start, newDocument.positionAt(htmlFormattedContent.length - afterFormatRangeLength));
let embeddedRanges = languageModes.getModesInRange(newDocument, newFormatRange);
let embeddedEdits: TextEdit[] = [];
for (let r of embeddedRanges) {
let mode = r.mode;
if (mode && mode.format && enabledModes[mode.getId()] && !r.attributeValue) {
let edits = await mode.format(newDocument, r, formattingOptions, settings);
for (let edit of edits) {
embeddedEdits.push(edit);
}
}
}
if (embeddedEdits.length === 0) {
pushAll(result, htmlEdits);
return result;
}
// apply all embedded format edits and create a single edit for all changes
let resultContent = TextDocument.applyEdits(newDocument, embeddedEdits);
let resultReplaceText = resultContent.substring(document.offsetAt(formatRange.start), resultContent.length - afterFormatRangeLength);
result.push(TextEdit.replace(formatRange, resultReplaceText));
return result;
} finally {
languageModes.onDocumentRemoved(newDocument);
}
}

View File

@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, FoldingRange, Position, Range, LanguageModes, LanguageMode } from './languageModes';
import { CancellationToken } from 'vscode-languageserver';
export async function getFoldingRanges(languageModes: LanguageModes, document: TextDocument, maxRanges: number | undefined, _cancellationToken: CancellationToken | null): Promise<FoldingRange[]> {
let htmlMode = languageModes.getMode('html');
let range = Range.create(Position.create(0, 0), Position.create(document.lineCount, 0));
let result: FoldingRange[] = [];
if (htmlMode && htmlMode.getFoldingRanges) {
result.push(... await htmlMode.getFoldingRanges(document));
}
// cache folding ranges per mode
let rangesPerMode: { [mode: string]: FoldingRange[] } = Object.create(null);
let getRangesForMode = async (mode: LanguageMode) => {
if (mode.getFoldingRanges) {
let ranges = rangesPerMode[mode.getId()];
if (!Array.isArray(ranges)) {
ranges = await mode.getFoldingRanges(document) || [];
rangesPerMode[mode.getId()] = ranges;
}
return ranges;
}
return [];
};
let modeRanges = languageModes.getModesInRange(document, range);
for (let modeRange of modeRanges) {
let mode = modeRange.mode;
if (mode && mode !== htmlMode && !modeRange.attributeValue) {
const ranges = await getRangesForMode(mode);
result.push(...ranges.filter(r => r.startLine >= modeRange.start.line && r.endLine < modeRange.end.line));
}
}
if (maxRanges && result.length > maxRanges) {
result = limitRanges(result, maxRanges);
}
return result;
}
function limitRanges(ranges: FoldingRange[], maxRanges: number) {
ranges = ranges.sort((r1, r2) => {
let diff = r1.startLine - r2.startLine;
if (diff === 0) {
diff = r1.endLine - r2.endLine;
}
return diff;
});
// compute each range's nesting level in 'nestingLevels'.
// count the number of ranges for each level in 'nestingLevelCounts'
let top: FoldingRange | undefined = undefined;
let previous: FoldingRange[] = [];
let nestingLevels: number[] = [];
let nestingLevelCounts: number[] = [];
let setNestingLevel = (index: number, level: number) => {
nestingLevels[index] = level;
if (level < 30) {
nestingLevelCounts[level] = (nestingLevelCounts[level] || 0) + 1;
}
};
// compute nesting levels and sanitize
for (let i = 0; i < ranges.length; i++) {
let entry = ranges[i];
if (!top) {
top = entry;
setNestingLevel(i, 0);
} else {
if (entry.startLine > top.startLine) {
if (entry.endLine <= top.endLine) {
previous.push(top);
top = entry;
setNestingLevel(i, previous.length);
} else if (entry.startLine > top.endLine) {
do {
top = previous.pop();
} while (top && entry.startLine > top.endLine);
if (top) {
previous.push(top);
}
top = entry;
setNestingLevel(i, previous.length);
}
}
}
}
let entries = 0;
let maxLevel = 0;
for (let i = 0; i < nestingLevelCounts.length; i++) {
let n = nestingLevelCounts[i];
if (n) {
if (n + entries > maxRanges) {
maxLevel = i;
break;
}
entries += n;
}
}
let result = [];
for (let i = 0; i < ranges.length; i++) {
let level = nestingLevels[i];
if (typeof level === 'number') {
if (level < maxLevel || (level === maxLevel && entries++ < maxRanges)) {
result.push(ranges[i]);
}
}
}
return result;
}

View File

@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getLanguageModelCache } from '../languageModelCache';
import {
LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions,
HTMLFormatConfiguration, SelectionRange,
TextDocument, Position, Range, FoldingRange,
LanguageMode, Workspace
} from './languageModes';
export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode {
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
return {
getId() {
return 'html';
},
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
return htmlLanguageService.getSelectionRanges(document, [position])[0];
},
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) {
let options = settings && settings.html && settings.html.suggest;
let doAutoComplete = settings && settings.html && settings.html.autoClosingTags;
if (doAutoComplete) {
options.hideAutoCompleteProposals = true;
}
const htmlDocument = htmlDocuments.get(document);
let completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options);
return completionList;
},
async doHover(document: TextDocument, position: Position) {
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
},
async findDocumentHighlight(document: TextDocument, position: Position) {
return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document));
},
async findDocumentLinks(document: TextDocument, documentContext: DocumentContext) {
return htmlLanguageService.findDocumentLinks(document, documentContext);
},
async findDocumentSymbols(document: TextDocument) {
return htmlLanguageService.findDocumentSymbols(document, htmlDocuments.get(document));
},
async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings = workspace.settings) {
let formatSettings: HTMLFormatConfiguration = settings && settings.html && settings.html.format;
if (formatSettings) {
formatSettings = merge(formatSettings, {});
} else {
formatSettings = {};
}
if (formatSettings.contentUnformatted) {
formatSettings.contentUnformatted = formatSettings.contentUnformatted + ',script';
} else {
formatSettings.contentUnformatted = 'script';
}
formatSettings = merge(formatParams, formatSettings);
return htmlLanguageService.format(document, range, formatSettings);
},
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
return htmlLanguageService.getFoldingRanges(document);
},
async doAutoClose(document: TextDocument, position: Position) {
let offset = document.offsetAt(position);
let text = document.getText();
if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) {
return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document));
}
return null;
},
async doRename(document: TextDocument, position: Position, newName: string) {
const htmlDocument = htmlDocuments.get(document);
return htmlLanguageService.doRename(document, position, newName, htmlDocument);
},
async onDocumentRemoved(document: TextDocument) {
htmlDocuments.onDocumentRemoved(document);
},
async findMatchingTagPosition(document: TextDocument, position: Position) {
const htmlDocument = htmlDocuments.get(document);
return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument);
},
async doOnTypeRename(document: TextDocument, position: Position) {
const htmlDocument = htmlDocuments.get(document);
return htmlLanguageService.findOnTypeRenameRanges(document, position, htmlDocument);
},
dispose() {
htmlDocuments.dispose();
}
};
}
function merge(src: any, dst: any): any {
for (const key in src) {
if (src.hasOwnProperty(key)) {
dst[key] = src[key];
}
}
return dst;
}

View File

@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join, basename, dirname } from 'path';
import { readFileSync } from 'fs';
const contents: { [name: string]: string } = {};
const serverFolder = basename(__dirname) === 'dist' ? dirname(__dirname) : dirname(dirname(__dirname));
const TYPESCRIPT_LIB_SOURCE = join(serverFolder, '../../node_modules/typescript/lib');
const JQUERY_PATH = join(serverFolder, 'lib/jquery.d.ts');
export function loadLibrary(name: string) {
let content = contents[name];
if (typeof content !== 'string') {
let libPath;
if (name === 'jquery') {
libPath = JQUERY_PATH;
} else {
libPath = join(TYPESCRIPT_LIB_SOURCE, name); // from source
}
try {
content = readFileSync(libPath).toString();
} catch (e) {
console.log(`Unable to load library ${name} at ${libPath}: ${e.message}`);
content = '';
}
contents[name] = content;
}
return content;
}

View File

@ -0,0 +1,477 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import {
SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation,
Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString,
DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange,
LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext
} from './languageModes';
import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings';
import { HTMLDocumentRegions } from './embeddedSupport';
import * as ts from 'typescript';
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false };
let currentTextDocument = TextDocument.create('init', 'javascript', 1, '');
const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => {
const host: ts.LanguageServiceHost = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () => [currentTextDocument.uri, 'jquery'],
getScriptKind: (fileName) => {
if (fileName === currentTextDocument.uri) {
return scriptKind;
}
return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
},
getScriptVersion: (fileName: string) => {
if (fileName === currentTextDocument.uri) {
return String(currentTextDocument.version);
}
return '1'; // default lib an jquery.d.ts are static
},
getScriptSnapshot: (fileName: string) => {
let text = '';
if (fileName === currentTextDocument.uri) {
text = currentTextDocument.getText();
} else {
text = libs.loadLibrary(fileName);
}
return {
getText: (start, end) => text.substring(start, end),
getLength: () => text.length,
getChangeRange: () => undefined
};
},
getCurrentDirectory: () => '',
getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6'
};
return ts.createLanguageService(host);
});
return {
async getLanguageService(jsDocument: TextDocument): Promise<ts.LanguageService> {
currentTextDocument = jsDocument;
return jsLanguageService;
},
getCompilationSettings() {
return compilerOptions;
},
dispose() {
if (jsLanguageService) {
jsLanguageService.then(s => s.dispose());
}
}
};
}
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode {
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId));
const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS);
let globalSettings: Settings = {};
return {
getId() {
return languageId;
},
async doValidation(document: TextDocument, settings = workspace.settings): Promise<Diagnostic[]> {
host.getCompilationSettings()['experimentalDecorators'] = settings && settings.javascript && settings.javascript.implicitProjectConfig.experimentalDecorators;
const jsDocument = jsDocuments.get(document);
const languageService = await host.getLanguageService(jsDocument);
const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri);
const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri);
return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => {
return {
range: convertRange(jsDocument, diag),
severity: DiagnosticSeverity.Error,
source: languageId,
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
};
});
},
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let offset = jsDocument.offsetAt(position);
let completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
if (!completions) {
return { isIncomplete: false, items: [] };
}
let replaceRange = convertRange(jsDocument, getWordAtText(jsDocument.getText(), offset, JS_WORD_REGEX));
return {
isIncomplete: false,
items: completions.entries.map(entry => {
return {
uri: document.uri,
position: position,
label: entry.name,
sortText: entry.sortText,
kind: convertKind(entry.kind),
textEdit: TextEdit.replace(replaceRange, entry.name),
data: { // data used for resolving item details (see 'doResolve')
languageId,
uri: document.uri,
offset: offset
}
};
})
};
},
async doResolve(document: TextDocument, item: CompletionItem): Promise<CompletionItem> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined);
if (details) {
item.detail = ts.displayPartsToString(details.displayParts);
item.documentation = ts.displayPartsToString(details.documentation);
delete item.data;
}
return item;
},
async doHover(document: TextDocument, position: Position): Promise<Hover | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (info) {
let contents = ts.displayPartsToString(info.displayParts);
return {
range: convertRange(jsDocument, info.textSpan),
contents: MarkedString.fromPlainText(contents)
};
}
return null;
},
async doSignatureHelp(document: TextDocument, position: Position): Promise<SignatureHelp | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined);
if (signHelp) {
let ret: SignatureHelp = {
activeSignature: signHelp.selectedItemIndex,
activeParameter: signHelp.argumentIndex,
signatures: []
};
signHelp.items.forEach(item => {
let signature: SignatureInformation = {
label: '',
documentation: undefined,
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;
},
async findDocumentHighlight(document: TextDocument, position: Position): Promise<DocumentHighlight[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]);
const out: DocumentHighlight[] = [];
for (const entry of highlights || []) {
for (const highlight of entry.highlightSpans) {
out.push({
range: convertRange(jsDocument, highlight.textSpan),
kind: highlight.kind === 'writtenReference' ? DocumentHighlightKind.Write : DocumentHighlightKind.Text
});
}
}
return out;
},
async findDocumentSymbols(document: TextDocument): Promise<SymbolInformation[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let items = jsLanguageService.getNavigationBarItems(jsDocument.uri);
if (items) {
let result: SymbolInformation[] = [];
let existing = Object.create(null);
let collectSymbols = (item: ts.NavigationBarItem, containerLabel?: string) => {
let sig = item.text + item.kind + item.spans[0].start;
if (item.kind !== 'script' && !existing[sig]) {
let symbol: SymbolInformation = {
name: item.text,
kind: convertSymbolKind(item.kind),
location: {
uri: document.uri,
range: convertRange(jsDocument, item.spans[0])
},
containerName: containerLabel
};
existing[sig] = true;
result.push(symbol);
containerLabel = item.text;
}
if (item.childItems && item.childItems.length > 0) {
for (let child of item.childItems) {
collectSymbols(child, containerLabel);
}
}
};
items.forEach(item => collectSymbols(item));
return result;
}
return [];
},
async findDefinition(document: TextDocument, position: Position): Promise<Definition | null> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (definition) {
return definition.filter(d => d.fileName === jsDocument.uri).map(d => {
return {
uri: document.uri,
range: convertRange(jsDocument, d.textSpan)
};
});
}
return null;
},
async findReferences(document: TextDocument, position: Position): Promise<Location[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
if (references) {
return references.filter(d => d.fileName === jsDocument.uri).map(d => {
return {
uri: document.uri,
range: convertRange(jsDocument, d.textSpan)
};
});
}
return [];
},
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
function convertSelectionRange(selectionRange: ts.SelectionRange): SelectionRange {
const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined;
return SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent);
}
const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position));
return convertSelectionRange(range);
},
async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): Promise<TextEdit[]> {
const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true);
const jsLanguageService = await host.getLanguageService(jsDocument);
let formatterSettings = settings && settings.javascript && settings.javascript.format;
let initialIndentLevel = computeInitialIndent(document, range, formatParams);
let formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1);
let start = jsDocument.offsetAt(range.start);
let end = jsDocument.offsetAt(range.end);
let lastLineRange = null;
if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(jsDocument.getText().substr(end - range.end.character, range.end.character)))) {
end -= range.end.character;
lastLineRange = Range.create(Position.create(range.end.line, 0), range.end);
}
let edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings);
if (edits) {
let result = [];
for (let edit of edits) {
if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
result.push({
range: convertRange(jsDocument, edit.span),
newText: edit.newText
});
}
}
if (lastLineRange) {
result.push({
range: lastLineRange,
newText: generateIndent(initialIndentLevel, formatParams)
});
}
return result;
}
return [];
},
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
let spans = jsLanguageService.getOutliningSpans(jsDocument.uri);
let ranges: FoldingRange[] = [];
for (let span of spans) {
let curr = convertRange(jsDocument, span.textSpan);
let startLine = curr.start.line;
let endLine = curr.end.line;
if (startLine < endLine) {
let foldingRange: FoldingRange = { startLine, endLine };
let match = document.getText(curr).match(/^\s*\/(?:(\/\s*#(?:end)?region\b)|(\*|\/))/);
if (match) {
foldingRange.kind = match[1] ? FoldingRangeKind.Region : FoldingRangeKind.Comment;
}
ranges.push(foldingRange);
}
}
return ranges;
},
onDocumentRemoved(document: TextDocument) {
jsDocuments.onDocumentRemoved(document);
},
async getSemanticTokens(document: TextDocument): Promise<SemanticTokenData[]> {
const jsDocument = jsDocuments.get(document);
const jsLanguageService = await host.getLanguageService(jsDocument);
return getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri);
},
getSemanticTokenLegend(): { types: string[], modifiers: string[] } {
return getSemanticTokenLegend();
},
dispose() {
host.dispose();
jsDocuments.dispose();
}
};
}
function convertRange(document: TextDocument, span: { start: number | undefined, length: number | undefined }): Range {
if (typeof span.start === 'undefined') {
const pos = document.positionAt(0);
return Range.create(pos, pos);
}
const startPosition = document.positionAt(span.start);
const endPosition = document.positionAt(span.start + (span.length || 0));
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 convertSymbolKind(kind: string): SymbolKind {
switch (kind) {
case 'var':
case 'local var':
case 'const':
return SymbolKind.Variable;
case 'function':
case 'local function':
return SymbolKind.Function;
case 'enum':
return SymbolKind.Enum;
case 'module':
return SymbolKind.Module;
case 'class':
return SymbolKind.Class;
case 'interface':
return SymbolKind.Interface;
case 'method':
return SymbolKind.Method;
case 'property':
case 'getter':
case 'setter':
return SymbolKind.Property;
}
return SymbolKind.Variable;
}
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: 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),
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces),
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);
}
function generateIndent(level: number, options: FormattingOptions) {
if (options.insertSpaces) {
return repeat(' ', level * options.tabSize);
} else {
return repeat('\t', level);
}
}

View File

@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, SemanticTokenData } from './languageModes';
import * as ts from 'typescript';
export function getSemanticTokenLegend() {
if (tokenTypes.length !== TokenType._) {
console.warn('TokenType has added new entries.');
}
if (tokenModifiers.length !== TokenModifier._) {
console.warn('TokenModifier has added new entries.');
}
return { types: tokenTypes, modifiers: tokenModifiers };
}
export function getSemanticTokens(jsLanguageService: ts.LanguageService, currentTextDocument: TextDocument, fileName: string): SemanticTokenData[] {
//https://ts-ast-viewer.com/#code/AQ0g2CmAuwGbALzAJwG4BQZQGNwEMBnQ4AQQEYBmYAb2C22zgEtJwATJVTRxgcwD27AQAp8AGmAAjAJS0A9POB8+7NQ168oscAJz5wANXwAnLug2bsJmAFcTAO2XAA1MHyvgu-UdOeWbOw8ViAAvpagocBAA
let resultTokens: SemanticTokenData[] = [];
const collector = (node: ts.Node, typeIdx: number, modifierSet: number) => {
resultTokens.push({ start: currentTextDocument.positionAt(node.getStart()), length: node.getWidth(), typeIdx, modifierSet });
};
collectTokens(jsLanguageService, fileName, { start: 0, length: currentTextDocument.getText().length }, collector);
return resultTokens;
}
function collectTokens(jsLanguageService: ts.LanguageService, fileName: string, span: ts.TextSpan, collector: (node: ts.Node, tokenType: number, tokenModifier: number) => void) {
const program = jsLanguageService.getProgram();
if (program) {
const typeChecker = program.getTypeChecker();
function visit(node: ts.Node) {
if (!node || !ts.textSpanIntersectsWith(span, node.pos, node.getFullWidth())) {
return;
}
if (ts.isIdentifier(node)) {
let symbol = typeChecker.getSymbolAtLocation(node);
if (symbol) {
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
let typeIdx = classifySymbol(symbol);
if (typeIdx !== undefined) {
let modifierSet = 0;
if (node.parent) {
const parentTypeIdx = tokenFromDeclarationMapping[node.parent.kind];
if (parentTypeIdx === typeIdx && (<ts.NamedDeclaration>node.parent).name === node) {
modifierSet = 1 << TokenModifier.declaration;
}
}
const decl = symbol.valueDeclaration;
const modifiers = decl ? ts.getCombinedModifierFlags(decl) : 0;
const nodeFlags = decl ? ts.getCombinedNodeFlags(decl) : 0;
if (modifiers & ts.ModifierFlags.Static) {
modifierSet |= 1 << TokenModifier.static;
}
if (modifiers & ts.ModifierFlags.Async) {
modifierSet |= 1 << TokenModifier.async;
}
if ((modifiers & ts.ModifierFlags.Readonly) || (nodeFlags & ts.NodeFlags.Const) || (symbol.getFlags() & ts.SymbolFlags.EnumMember)) {
modifierSet |= 1 << TokenModifier.readonly;
}
collector(node, typeIdx, modifierSet);
}
}
}
ts.forEachChild(node, visit);
}
const sourceFile = program.getSourceFile(fileName);
if (sourceFile) {
visit(sourceFile);
}
}
}
function classifySymbol(symbol: ts.Symbol) {
const flags = symbol.getFlags();
if (flags & ts.SymbolFlags.Class) {
return TokenType.class;
} else if (flags & ts.SymbolFlags.Enum) {
return TokenType.enum;
} else if (flags & ts.SymbolFlags.TypeAlias) {
return TokenType.type;
} else if (flags & ts.SymbolFlags.Type) {
if (flags & ts.SymbolFlags.Interface) {
return TokenType.interface;
} if (flags & ts.SymbolFlags.TypeParameter) {
return TokenType.typeParameter;
}
}
const decl = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
return decl && tokenFromDeclarationMapping[decl.kind];
}
export const enum TokenType {
class, enum, interface, namespace, typeParameter, type, parameter, variable, property, function, member, _
}
export const enum TokenModifier {
declaration, static, async, readonly, _
}
const tokenTypes: string[] = [];
tokenTypes[TokenType.class] = 'class';
tokenTypes[TokenType.enum] = 'enum';
tokenTypes[TokenType.interface] = 'interface';
tokenTypes[TokenType.namespace] = 'namespace';
tokenTypes[TokenType.typeParameter] = 'typeParameter';
tokenTypes[TokenType.type] = 'type';
tokenTypes[TokenType.parameter] = 'parameter';
tokenTypes[TokenType.variable] = 'variable';
tokenTypes[TokenType.property] = 'property';
tokenTypes[TokenType.function] = 'function';
tokenTypes[TokenType.member] = 'member';
const tokenModifiers: string[] = [];
tokenModifiers[TokenModifier.async] = 'async';
tokenModifiers[TokenModifier.declaration] = 'declaration';
tokenModifiers[TokenModifier.readonly] = 'readonly';
tokenModifiers[TokenModifier.static] = 'static';
const tokenFromDeclarationMapping: { [name: string]: TokenType } = {
[ts.SyntaxKind.VariableDeclaration]: TokenType.variable,
[ts.SyntaxKind.Parameter]: TokenType.parameter,
[ts.SyntaxKind.PropertyDeclaration]: TokenType.property,
[ts.SyntaxKind.ModuleDeclaration]: TokenType.namespace,
[ts.SyntaxKind.EnumDeclaration]: TokenType.enum,
[ts.SyntaxKind.EnumMember]: TokenType.property,
[ts.SyntaxKind.ClassDeclaration]: TokenType.class,
[ts.SyntaxKind.MethodDeclaration]: TokenType.member,
[ts.SyntaxKind.FunctionDeclaration]: TokenType.function,
[ts.SyntaxKind.MethodSignature]: TokenType.member,
[ts.SyntaxKind.GetAccessor]: TokenType.property,
[ts.SyntaxKind.PropertySignature]: TokenType.property,
[ts.SyntaxKind.InterfaceDeclaration]: TokenType.interface,
[ts.SyntaxKind.TypeAliasDeclaration]: TokenType.type,
[ts.SyntaxKind.TypeParameter]: TokenType.typeParameter
};

View File

@ -0,0 +1,162 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getCSSLanguageService } from 'vscode-css-languageservice';
import {
ClientCapabilities, DocumentContext, getLanguageService as getHTMLLanguageService, IHTMLDataProvider, SelectionRange,
CompletionItem, CompletionList, Definition, Diagnostic, DocumentHighlight, DocumentLink, FoldingRange, FormattingOptions,
Hover, Location, Position, Range, SignatureHelp, SymbolInformation, TextDocument, TextEdit,
Color, ColorInformation, ColorPresentation, WorkspaceEdit
} from 'vscode-html-languageservice';
import { WorkspaceFolder } from 'vscode-languageserver';
import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache';
import { getCSSMode } from './cssMode';
import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport';
import { getHTMLMode } from './htmlMode';
import { getJavaScriptMode } from './javascriptMode';
import { RequestService } from '../requests';
export * from 'vscode-html-languageservice';
export { WorkspaceFolder } from 'vscode-languageserver';
export interface Settings {
css?: any;
html?: any;
javascript?: any;
}
export interface Workspace {
readonly settings: Settings;
readonly folders: WorkspaceFolder[];
}
export interface SemanticTokenData {
start: Position;
length: number;
typeIdx: number;
modifierSet: number;
}
export interface LanguageMode {
getId(): string;
getSelectionRange?: (document: TextDocument, position: Position) => Promise<SelectionRange>;
doValidation?: (document: TextDocument, settings?: Settings) => Promise<Diagnostic[]>;
doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise<CompletionList>;
doResolve?: (document: TextDocument, item: CompletionItem) => Promise<CompletionItem>;
doHover?: (document: TextDocument, position: Position) => Promise<Hover | null>;
doSignatureHelp?: (document: TextDocument, position: Position) => Promise<SignatureHelp | null>;
doRename?: (document: TextDocument, position: Position, newName: string) => Promise<WorkspaceEdit | null>;
doOnTypeRename?: (document: TextDocument, position: Position) => Promise<Range[] | null>;
findDocumentHighlight?: (document: TextDocument, position: Position) => Promise<DocumentHighlight[]>;
findDocumentSymbols?: (document: TextDocument) => Promise<SymbolInformation[]>;
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => Promise<DocumentLink[]>;
findDefinition?: (document: TextDocument, position: Position) => Promise<Definition | null>;
findReferences?: (document: TextDocument, position: Position) => Promise<Location[]>;
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => Promise<TextEdit[]>;
findDocumentColors?: (document: TextDocument) => Promise<ColorInformation[]>;
getColorPresentations?: (document: TextDocument, color: Color, range: Range) => Promise<ColorPresentation[]>;
doAutoClose?: (document: TextDocument, position: Position) => Promise<string | null>;
findMatchingTagPosition?: (document: TextDocument, position: Position) => Promise<Position | null>;
getFoldingRanges?: (document: TextDocument) => Promise<FoldingRange[]>;
onDocumentRemoved(document: TextDocument): void;
getSemanticTokens?(document: TextDocument): Promise<SemanticTokenData[]>;
getSemanticTokenLegend?(): { types: string[], modifiers: string[] };
dispose(): void;
}
export interface LanguageModes {
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined;
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
getAllModes(): LanguageMode[];
getAllModesInDocument(document: TextDocument): LanguageMode[];
getMode(languageId: string): LanguageMode | undefined;
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}
export interface LanguageModeRange extends Range {
mode: LanguageMode | undefined;
attributeValue?: boolean;
}
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes {
const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService });
const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService });
let documentRegions = getLanguageModelCache<HTMLDocumentRegions>(10, 60, document => getDocumentRegions(htmlLanguageService, document));
let modelCaches: LanguageModelCache<any>[] = [];
modelCaches.push(documentRegions);
let modes = Object.create(null);
modes['html'] = getHTMLMode(htmlLanguageService, workspace);
if (supportedLanguages['css']) {
modes['css'] = getCSSMode(cssLanguageService, documentRegions, workspace);
}
if (supportedLanguages['javascript']) {
modes['javascript'] = getJavaScriptMode(documentRegions, 'javascript', workspace);
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace);
}
return {
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {
htmlLanguageService.setDataProviders(true, dataProviders);
},
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
let languageId = documentRegions.get(document).getLanguageAtPosition(position);
if (languageId) {
return modes[languageId];
}
return undefined;
},
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] {
return documentRegions.get(document).getLanguageRanges(range).map(r => {
return <LanguageModeRange>{
start: r.start,
end: r.end,
mode: r.languageId && modes[r.languageId],
attributeValue: r.attributeValue
};
});
},
getAllModesInDocument(document: TextDocument): LanguageMode[] {
let result = [];
for (let languageId of documentRegions.get(document).getLanguagesInDocument()) {
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];
},
onDocumentRemoved(document: TextDocument) {
modelCaches.forEach(mc => mc.onDocumentRemoved(document));
for (let mode in modes) {
modes[mode].onDocumentRemoved(document);
}
},
dispose(): void {
modelCaches.forEach(mc => mc.dispose());
modelCaches = [];
for (let mode in modes) {
modes[mode].dispose();
}
modes = {};
}
};
}

View File

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LanguageModes, TextDocument, Position, Range, SelectionRange } from './languageModes';
import { insideRangeButNotSame } from '../utils/positions';
export async function getSelectionRanges(languageModes: LanguageModes, document: TextDocument, positions: Position[]) {
const htmlMode = languageModes.getMode('html');
return Promise.all(positions.map(async position => {
const htmlRange = await htmlMode!.getSelectionRange!(document, position);
const mode = languageModes.getModeAtPosition(document, position);
if (mode && mode.getSelectionRange) {
let range = await mode.getSelectionRange(document, position);
let top = range;
while (top.parent && insideRangeButNotSame(htmlRange.range, top.parent.range)) {
top = top.parent;
}
top.parent = htmlRange;
return range;
}
return htmlRange || SelectionRange.create(Range.create(position, position));
}));
}

View File

@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SemanticTokenData, Range, TextDocument, LanguageModes, Position } from './languageModes';
import { beforeOrSame } from '../utils/positions';
interface LegendMapping {
types: number[] | undefined;
modifiers: number[] | undefined;
}
export interface SemanticTokenProvider {
readonly legend: { types: string[]; modifiers: string[] };
getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise<number[]>;
}
export function newSemanticTokenProvider(languageModes: LanguageModes): SemanticTokenProvider {
// combined legend across modes
const legend: { types: string[], modifiers: string[] } = { types: [], modifiers: [] };
const legendMappings: { [modeId: string]: LegendMapping } = {};
for (let mode of languageModes.getAllModes()) {
if (mode.getSemanticTokenLegend && mode.getSemanticTokens) {
const modeLegend = mode.getSemanticTokenLegend();
legendMappings[mode.getId()] = { types: createMapping(modeLegend.types, legend.types), modifiers: createMapping(modeLegend.modifiers, legend.modifiers) };
}
}
return {
legend,
async getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise<number[]> {
const allTokens: SemanticTokenData[] = [];
for (let mode of languageModes.getAllModesInDocument(document)) {
if (mode.getSemanticTokens) {
const mapping = legendMappings[mode.getId()];
const tokens = await mode.getSemanticTokens(document);
applyTypesMapping(tokens, mapping.types);
applyModifiersMapping(tokens, mapping.modifiers);
for (let token of tokens) {
allTokens.push(token);
}
}
}
return encodeTokens(allTokens, ranges);
}
};
}
function createMapping(origLegend: string[], newLegend: string[]): number[] | undefined {
const mapping: number[] = [];
let needsMapping = false;
for (let origIndex = 0; origIndex < origLegend.length; origIndex++) {
const entry = origLegend[origIndex];
let newIndex = newLegend.indexOf(entry);
if (newIndex === -1) {
newIndex = newLegend.length;
newLegend.push(entry);
}
mapping.push(newIndex);
needsMapping = needsMapping || (newIndex !== origIndex);
}
return needsMapping ? mapping : undefined;
}
function applyTypesMapping(tokens: SemanticTokenData[], typesMapping: number[] | undefined): void {
if (typesMapping) {
for (let token of tokens) {
token.typeIdx = typesMapping[token.typeIdx];
}
}
}
function applyModifiersMapping(tokens: SemanticTokenData[], modifiersMapping: number[] | undefined): void {
if (modifiersMapping) {
for (let token of tokens) {
let modifierSet = token.modifierSet;
if (modifierSet) {
let index = 0;
let result = 0;
while (modifierSet > 0) {
if ((modifierSet & 1) !== 0) {
result = result + (1 << modifiersMapping[index]);
}
index++;
modifierSet = modifierSet >> 1;
}
token.modifierSet = result;
}
}
}
}
const fullRange = [Range.create(Position.create(0, 0), Position.create(Number.MAX_VALUE, 0))];
function encodeTokens(tokens: SemanticTokenData[], ranges?: Range[]): number[] {
const resultTokens = tokens.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character);
if (ranges) {
ranges = ranges.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character);
} else {
ranges = fullRange;
}
let rangeIndex = 0;
let currRange = ranges[rangeIndex++];
let prefLine = 0;
let prevChar = 0;
let encodedResult: number[] = [];
for (let k = 0; k < resultTokens.length && currRange; k++) {
const curr = resultTokens[k];
const start = curr.start;
while (currRange && beforeOrSame(currRange.end, start)) {
currRange = ranges[rangeIndex++];
}
if (currRange && beforeOrSame(currRange.start, start) && beforeOrSame({ line: start.line, character: start.character + curr.length }, currRange.end)) {
// token inside a range
if (prefLine !== start.line) {
prevChar = 0;
}
encodedResult.push(start.line - prefLine); // line delta
encodedResult.push(start.character - prevChar); // line delta
encodedResult.push(curr.length); // length
encodedResult.push(curr.typeIdx); // tokenType
encodedResult.push(curr.modifierSet); // tokenModifier
prefLine = start.line;
prevChar = start.character;
}
}
return encodedResult;
}

View File

@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createConnection, Connection } from 'vscode-languageserver/node';
import { formatError } from '../utils/runner';
import { startServer } from '../htmlServer';
import { getNodeFSRequestService } from './nodeFs';
// Create a connection for the server.
const connection: Connection = createConnection();
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);
process.on('unhandledRejection', (e: any) => {
connection.console.error(formatError(`Unhandled exception`, e));
});
startServer(connection, { file: getNodeFSRequestService() });

View File

@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RequestService, getScheme } from '../requests';
import { URI as Uri } from 'vscode-uri';
import * as fs from 'fs';
import { FileType } from 'vscode-css-languageservice';
export function getNodeFSRequestService(): RequestService {
function ensureFileUri(location: string) {
if (getScheme(location) !== 'file') {
throw new Error('fileRequestService can only handle file URLs');
}
}
return {
getContent(location: string, encoding?: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.readFile(uri.fsPath, encoding, (err, buf) => {
if (err) {
return e(err);
}
c(buf.toString());
});
});
},
stat(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.stat(uri.fsPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
} else {
return e(err);
}
}
let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}
c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
},
readDirectory(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const path = Uri.parse(location).fsPath;
fs.readdir(path, { withFileTypes: true }, (err, children) => {
if (err) {
return e(err);
}
c(children.map(stat => {
if (stat.isSymbolicLink()) {
return [stat.name, FileType.SymbolicLink];
} else if (stat.isDirectory()) {
return [stat.name, FileType.Directory];
} else if (stat.isFile()) {
return [stat.name, FileType.File];
} else {
return [stat.name, FileType.Unknown];
}
}));
});
});
}
};
}

View File

@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vscode-uri';
import { RequestType, Connection } from 'vscode-languageserver';
import { RuntimeEnvironment } from './htmlServer';
export namespace FsContentRequest {
export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content');
}
export namespace FsStatRequest {
export const type: RequestType<string, FileStat, any, any> = new RequestType('fs/stat');
}
export namespace FsReadDirRequest {
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
}
export enum FileType {
/**
* The file type is unknown.
*/
Unknown = 0,
/**
* A regular file.
*/
File = 1,
/**
* A directory.
*/
Directory = 2,
/**
* A symbolic link to a file.
*/
SymbolicLink = 64
}
export interface FileStat {
/**
* The type of the file, e.g. is a regular file, a directory, or symbolic link
* to a file.
*/
type: FileType;
/**
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
/**
* The size in bytes.
*/
size: number;
}
export interface RequestService {
getContent(uri: string, encoding?: string): Promise<string>;
stat(uri: string): Promise<FileStat>;
readDirectory(uri: string): Promise<[string, FileType][]>;
}
export function getRequestService(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): RequestService {
const builtInHandlers: { [protocol: string]: RequestService | undefined } = {};
for (let protocol of handledSchemas) {
if (protocol === 'file') {
builtInHandlers[protocol] = runtime.file;
} else if (protocol === 'http' || protocol === 'https') {
builtInHandlers[protocol] = runtime.http;
}
}
return {
async stat(uri: string): Promise<FileStat> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.stat(uri);
}
const res = await connection.sendRequest(FsStatRequest.type, uri.toString());
return res;
},
readDirectory(uri: string): Promise<[string, FileType][]> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.readDirectory(uri);
}
return connection.sendRequest(FsReadDirRequest.type, uri.toString());
},
getContent(uri: string, encoding?: string): Promise<string> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.getContent(uri, encoding);
}
return connection.sendRequest(FsContentRequest.type, { uri: uri.toString(), encoding });
}
};
}
export function getScheme(uri: string) {
return uri.substr(0, uri.indexOf(':'));
}
export function dirname(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
}
export function basename(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return uri.substr(lastIndexOfSlash + 1);
}
const Slash = '/'.charCodeAt(0);
const Dot = '.'.charCodeAt(0);
export function extname(uri: string) {
for (let i = uri.length - 1; i >= 0; i--) {
const ch = uri.charCodeAt(i);
if (ch === Dot) {
if (i > 0 && uri.charCodeAt(i - 1) !== Slash) {
return uri.substr(i);
} else {
break;
}
} else if (ch === Slash) {
break;
}
}
return '';
}
export function isAbsolutePath(path: string) {
return path.charCodeAt(0) === Slash;
}
export function resolvePath(uriString: string, path: string): string {
if (isAbsolutePath(path)) {
const uri = URI.parse(uriString);
const parts = path.split('/');
return uri.with({ path: normalizePath(parts) }).toString();
}
return joinPath(uriString, path);
}
export function normalizePath(parts: string[]): string {
const newParts: string[] = [];
for (const part of parts) {
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
// ignore
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
newParts.pop();
} else {
newParts.push(part);
}
}
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
newParts.push('');
}
let res = newParts.join('/');
if (parts[0].length === 0) {
res = '/' + res;
}
return res;
}
export function joinPath(uriString: string, ...paths: string[]): string {
const uri = URI.parse(uriString);
const parts = uri.path.split('/');
for (let path of paths) {
parts.push(...path.split('/'));
}
return uri.with({ path: normalizePath(parts) }).toString();
}

View File

@ -0,0 +1,319 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import * as path from 'path';
import { URI } from 'vscode-uri';
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes';
import { getNodeFSRequestService } from '../node/nodeFs';
import { getDocumentContext } from '../utils/documentContext';
export interface ItemDescription {
label: string;
documentation?: string;
kind?: CompletionItemKind;
resultText?: string;
command?: { title: string, command: string };
notAvailable?: boolean;
}
export function assertCompletion(completions: CompletionList, expected: ItemDescription, document: TextDocument) {
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(', ')}`);
let match = matches[0];
if (expected.documentation) {
assert.equal(match.documentation, expected.documentation);
}
if (expected.kind) {
assert.equal(match.kind, expected.kind);
}
if (expected.resultText && match.textEdit) {
const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText);
assert.equal(TextDocument.applyEdits(document, [edit]), expected.resultText);
}
if (expected.command) {
assert.deepEqual(match.command, expected.command);
}
}
const testUri = 'test://test/test.html';
export async function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise<void> {
let offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
let workspace = {
settings: {},
folders: workspaceFolders || [{ name: 'x', uri: uri.substr(0, uri.lastIndexOf('/')) }]
};
let document = TextDocument.create(uri, 'html', 0, value);
let position = document.positionAt(offset);
const context = getDocumentContext(uri, workspace.folders);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const mode = languageModes.getModeAtPosition(document, position)!;
let list = await mode.doComplete!(document, position, context);
if (expected.count) {
assert.equal(list.items.length, expected.count);
}
if (expected.items) {
for (let item of expected.items) {
assertCompletion(list, item, document);
}
}
}
suite('HTML Completion', () => {
test('HTML JavaScript Completions', async () => {
await testCompletionFor('<html><script>window.|</script></html>', {
items: [
{ label: 'location', resultText: '<html><script>window.location</script></html>' },
]
});
await testCompletionFor('<html><script>$.|</script></html>', {
items: [
{ label: 'getJSON', resultText: '<html><script>$.getJSON</script></html>' },
]
});
await testCompletionFor('<html><script>const x = { a: 1 };</script><script>x.|</script></html>', {
items: [
{ label: 'a', resultText: '<html><script>const x = { a: 1 };</script><script>x.a</script></html>' },
]
}, 'test://test/test2.html');
});
});
suite('HTML Path Completion', () => {
const triggerSuggestCommand = {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
};
const fixtureRoot = path.resolve(__dirname, '../../src/test/pathCompletionFixtures');
const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() };
const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString();
const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString();
test('Basics - Correct label/kind/result/command', async () => {
await testCompletionFor('<script src="./|">', {
items: [
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: '<script src="./about/">', command: triggerSuggestCommand },
{ label: 'index.html', kind: CompletionItemKind.File, resultText: '<script src="./index.html">' },
{ label: 'src/', kind: CompletionItemKind.Folder, resultText: '<script src="./src/">', command: triggerSuggestCommand }
]
}, indexHtmlUri);
});
test('Basics - Single Quote', async () => {
await testCompletionFor(`<script src='./|'>`, {
items: [
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: `<script src='./about/'>`, command: triggerSuggestCommand },
{ label: 'index.html', kind: CompletionItemKind.File, resultText: `<script src='./index.html'>` },
{ label: 'src/', kind: CompletionItemKind.Folder, resultText: `<script src='./src/'>`, command: triggerSuggestCommand }
]
}, indexHtmlUri);
});
test('No completion for remote paths', async () => {
await testCompletionFor('<script src="http:">', { items: [] });
await testCompletionFor('<script src="http:/|">', { items: [] });
await testCompletionFor('<script src="http://|">', { items: [] });
await testCompletionFor('<script src="https:|">', { items: [] });
await testCompletionFor('<script src="https:/|">', { items: [] });
await testCompletionFor('<script src="https://|">', { items: [] });
await testCompletionFor('<script src="//|">', { items: [] });
});
test('Relative Path', async () => {
await testCompletionFor('<script src="../|">', {
items: [
{ label: 'about/', resultText: '<script src="../about/">' },
{ label: 'index.html', resultText: '<script src="../index.html">' },
{ label: 'src/', resultText: '<script src="../src/">' }
]
}, aboutHtmlUri);
await testCompletionFor('<script src="../src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
]
}, aboutHtmlUri);
});
test('Absolute Path', async () => {
await testCompletionFor('<script src="/|">', {
items: [
{ label: 'about/', resultText: '<script src="/about/">' },
{ label: 'index.html', resultText: '<script src="/index.html">' },
{ label: 'src/', resultText: '<script src="/src/">' },
]
}, indexHtmlUri);
await testCompletionFor('<script src="/src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('Empty Path Value', async () => {
// document: index.html
await testCompletionFor('<script src="|">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
{ label: 'src/', resultText: '<script src="src/">' },
]
}, indexHtmlUri);
// document: about.html
await testCompletionFor('<script src="|">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
{ label: 'media/', resultText: '<script src="media/">' },
]
}, aboutHtmlUri);
});
test('Incomplete Path', async () => {
await testCompletionFor('<script src="/src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="../src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('No leading dot or slash', async () => {
// document: index.html
await testCompletionFor('<script src="s|">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
{ label: 'src/', resultText: '<script src="src/">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
// document: about.html
await testCompletionFor('<script src="s|">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
{ label: 'media/', resultText: '<script src="media/">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="media/|">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="media/f|">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('Trigger completion in middle of path', async () => {
// document: index.html
await testCompletionFor('<script src="src/f|eature.js">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="s|rc/feature.js">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
{ label: 'src/', resultText: '<script src="src/">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
// document: about.html
await testCompletionFor('<script src="media/f|eature.js">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="m|edia/feature.js">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
{ label: 'media/', resultText: '<script src="media/">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('Trigger completion in middle of path and with whitespaces', async () => {
await testCompletionFor('<script src="./| about/about.html>', {
items: [
{ label: 'about/', resultText: '<script src="./about/ about/about.html>' },
{ label: 'index.html', resultText: '<script src="./index.html about/about.html>' },
{ label: 'src/', resultText: '<script src="./src/ about/about.html>' },
]
}, indexHtmlUri, [fixtureWorkspace]);
await testCompletionFor('<script src="./a|bout /about.html>', {
items: [
{ label: 'about/', resultText: '<script src="./about/ /about.html>' },
{ label: 'index.html', resultText: '<script src="./index.html /about.html>' },
{ label: 'src/', resultText: '<script src="./src/ /about.html>' },
]
}, indexHtmlUri, [fixtureWorkspace]);
});
test('Completion should ignore files/folders starting with dot', async () => {
await testCompletionFor('<script src="./|"', {
count: 3
}, indexHtmlUri, [fixtureWorkspace]);
});
test('Unquoted Path', async () => {
/* Unquoted value is not supported in html language service yet
testCompletionFor(`<div><a href=about/|>`, {
items: [
{ label: 'about.html', resultText: `<div><a href=about/about.html>` }
]
}, testUri);
*/
});
});

View File

@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { getDocumentContext } from '../utils/documentContext';
suite('HTML Document Context', () => {
test('Context', function (): any {
const docURI = 'file:///users/test/folder/test.html';
const rootFolders = [{ name: '', uri: 'file:///users/test/' }];
let context = getDocumentContext(docURI, rootFolders);
assert.equal(context.resolveReference('/', docURI), 'file:///users/test/');
assert.equal(context.resolveReference('/message.html', docURI), 'file:///users/test/message.html');
assert.equal(context.resolveReference('message.html', docURI), 'file:///users/test/folder/message.html');
assert.equal(context.resolveReference('message.html', 'file:///users/test/'), 'file:///users/test/message.html');
});
});

View File

@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import * as embeddedSupport from '../modes/embeddedSupport';
import { getLanguageService } from 'vscode-html-languageservice';
import { TextDocument } from '../modes/languageModes';
suite('HTML Embedded Support', () => {
const htmlLanguageService = getLanguageService();
function assertLanguageId(value: string, expectedLanguageId: string | undefined): void {
const offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
const document = TextDocument.create('test://test/test.html', 'html', 0, value);
const position = document.positionAt(offset);
const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
const languageId = docRegions.getLanguageAtPosition(position);
assert.equal(languageId, expectedLanguageId);
}
function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void {
const document = TextDocument.create('test://test/test.html', 'html', 0, value);
const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
const content = docRegions.getEmbeddedDocument(languageId);
assert.equal(content.getText(), expectedContent);
}
test('Styles', function (): any {
assertLanguageId('|<html><style>foo { }</style></html>', 'html');
assertLanguageId('<html|><style>foo { }</style></html>', 'html');
assertLanguageId('<html><st|yle>foo { }</style></html>', 'html');
assertLanguageId('<html><style>|foo { }</style></html>', 'css');
assertLanguageId('<html><style>foo| { }</style></html>', 'css');
assertLanguageId('<html><style>foo { }|</style></html>', 'css');
assertLanguageId('<html><style>foo { }</sty|le></html>', 'html');
});
test('Styles - Incomplete HTML', function (): any {
assertLanguageId('|<html><style>foo { }', 'html');
assertLanguageId('<html><style>fo|o { }', 'css');
assertLanguageId('<html><style>foo { }|', 'css');
});
test('Style in attribute', function (): any {
assertLanguageId('<div id="xy" |style="color: red"/>', 'html');
assertLanguageId('<div id="xy" styl|e="color: red"/>', 'html');
assertLanguageId('<div id="xy" style=|"color: red"/>', 'html');
assertLanguageId('<div id="xy" style="|color: red"/>', 'css');
assertLanguageId('<div id="xy" style="color|: red"/>', 'css');
assertLanguageId('<div id="xy" style="color: red|"/>', 'css');
assertLanguageId('<div id="xy" style="color: red"|/>', 'html');
assertLanguageId('<div id="xy" style=\'color: r|ed\'/>', 'css');
assertLanguageId('<div id="xy" style|=color:red/>', 'html');
assertLanguageId('<div id="xy" style=|color:red/>', 'css');
assertLanguageId('<div id="xy" style=color:r|ed/>', 'css');
assertLanguageId('<div id="xy" style=color:red|/>', 'css');
assertLanguageId('<div id="xy" style=color:red/|>', 'html');
});
test('Style content', function (): any {
assertEmbeddedLanguageContent('<html><style>foo { }</style></html>', 'css', ' foo { } ');
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'css', ' ');
assertEmbeddedLanguageContent('<html><style>foo { }</style>Hello<style>foo { }</style></html>', 'css', ' foo { } foo { } ');
assertEmbeddedLanguageContent('<html>\n <style>\n foo { } \n </style>\n</html>\n', 'css', '\n \n foo { } \n \n\n');
assertEmbeddedLanguageContent('<div style="color: red"></div>', 'css', ' __{color: red} ');
assertEmbeddedLanguageContent('<div style=color:red></div>', 'css', ' __{color:red} ');
});
test('Scripts', function (): any {
assertLanguageId('|<html><script>var i = 0;</script></html>', 'html');
assertLanguageId('<html|><script>var i = 0;</script></html>', 'html');
assertLanguageId('<html><scr|ipt>var i = 0;</script></html>', 'html');
assertLanguageId('<html><script>|var i = 0;</script></html>', 'javascript');
assertLanguageId('<html><script>var| i = 0;</script></html>', 'javascript');
assertLanguageId('<html><script>var i = 0;|</script></html>', 'javascript');
assertLanguageId('<html><script>var i = 0;</scr|ipt></html>', 'html');
assertLanguageId('<script type="text/javascript">var| i = 0;</script>', 'javascript');
assertLanguageId('<script type="text/ecmascript">var| i = 0;</script>', 'javascript');
assertLanguageId('<script type="application/javascript">var| i = 0;</script>', 'javascript');
assertLanguageId('<script type="application/ecmascript">var| i = 0;</script>', 'javascript');
assertLanguageId('<script type="application/typescript">var| i = 0;</script>', undefined);
assertLanguageId('<script type=\'text/javascript\'>var| i = 0;</script>', 'javascript');
});
test('Scripts in attribute', function (): any {
assertLanguageId('<div |onKeyUp="foo()" onkeydown=\'bar()\'/>', 'html');
assertLanguageId('<div onKeyUp=|"foo()" onkeydown=\'bar()\'/>', 'html');
assertLanguageId('<div onKeyUp="|foo()" onkeydown=\'bar()\'/>', 'javascript');
assertLanguageId('<div onKeyUp="foo(|)" onkeydown=\'bar()\'/>', 'javascript');
assertLanguageId('<div onKeyUp="foo()|" onkeydown=\'bar()\'/>', 'javascript');
assertLanguageId('<div onKeyUp="foo()"| onkeydown=\'bar()\'/>', 'html');
assertLanguageId('<div onKeyUp="foo()" onkeydown=|\'bar()\'/>', 'html');
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'|bar()\'/>', 'javascript');
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()|\'/>', 'javascript');
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()\'|/>', 'html');
assertLanguageId('<DIV ONKEYUP|=foo()</DIV>', 'html');
assertLanguageId('<DIV ONKEYUP=|foo()</DIV>', 'javascript');
assertLanguageId('<DIV ONKEYUP=f|oo()</DIV>', 'javascript');
assertLanguageId('<DIV ONKEYUP=foo(|)</DIV>', 'javascript');
assertLanguageId('<DIV ONKEYUP=foo()|</DIV>', 'javascript');
assertLanguageId('<DIV ONKEYUP=foo()<|/DIV>', 'html');
assertLanguageId('<label data-content="|Checkbox"/>', 'html');
assertLanguageId('<label on="|Checkbox"/>', 'html');
});
test('Script content', function (): any {
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'javascript', ' var i = 0; ');
assertEmbeddedLanguageContent('<script type="text/javascript">var i = 0;</script>', 'javascript', ' var i = 0; ');
assertEmbeddedLanguageContent('<div onKeyUp="foo()" onkeydown="bar()"/>', 'javascript', ' foo(); bar(); ');
});
});

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
Polymer({
is: "chat-messages",
properties: {
user: {},
friend: {
observer: "_friendChanged"
}
},
});
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
Polymer({
is: "chat-messages",
properties: {
user: {},
friend: {
observer: "_friendChanged"
}
},
});
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
Polymer({
is: "chat-messages",
properties: {
user: {},
friend: {
observer: "_friendChanged"
}
},
});
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,6 @@
<app-route path="/module" element="page-module" bindRouter onUrlChange="updateModel"></app-route>
<script>
Polymer({
});
</script>

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
Polymer({
is: "chat-messages",
properties: {
user: {},
friend: {
observer: "_friendChanged"
}
},
});
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,6 @@
<app-route path="/module" element="page-module" bindRouter onUrlChange="updateModel"></app-route>
<script>
Polymer({
});
</script>

View File

@ -0,0 +1,215 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { getFoldingRanges } from '../modes/htmlFolding';
import { TextDocument, getLanguageModes } from '../modes/languageModes';
import { ClientCapabilities } from 'vscode-css-languageservice';
import { getNodeFSRequestService } from '../node/nodeFs';
interface ExpectedIndentRange {
startLine: number;
endLine: number;
kind?: string;
}
async function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?: string, nRanges?: number): Promise<void> {
const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n'));
const workspace = {
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const actual = await getFoldingRanges(languageModes, document, nRanges, null);
let actualRanges = [];
for (let i = 0; i < actual.length; i++) {
actualRanges[i] = r(actual[i].startLine, actual[i].endLine, actual[i].kind);
}
actualRanges = actualRanges.sort((r1, r2) => r1.startLine - r2.startLine);
assert.deepEqual(actualRanges, expected, message);
}
function r(startLine: number, endLine: number, kind?: string): ExpectedIndentRange {
return { startLine, endLine, kind };
}
suite('HTML Folding', async () => {
test('Embedded JavaScript', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script>',
/*3*/'function f() {',
/*4*/'}',
/*5*/'</script>',
/*6*/'</head>',
/*7*/'</html>',
];
await await assertRanges(input, [r(0, 6), r(1, 5), r(2, 4), r(3, 4)]);
});
test('Embedded JavaScript - multiple areas', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head>',
/* 2*/'<script>',
/* 3*/' var x = {',
/* 4*/' foo: true,',
/* 5*/' bar: {}',
/* 6*/' };',
/* 7*/'</script>',
/* 8*/'<script>',
/* 9*/' test(() => { // hello',
/*10*/' f();',
/*11*/' });',
/*12*/'</script>',
/*13*/'</head>',
/*14*/'</html>',
];
await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11), r(9, 11)]);
});
test('Embedded JavaScript - incomplete', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head>',
/* 2*/'<script>',
/* 3*/' var x = {',
/* 4*/'</script>',
/* 5*/'<script>',
/* 6*/' });',
/* 7*/'</script>',
/* 8*/'</head>',
/* 9*/'</html>',
];
await assertRanges(input, [r(0, 8), r(1, 7), r(2, 3), r(5, 6)]);
});
test('Embedded JavaScript - regions', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head>',
/* 2*/'<script>',
/* 3*/' // #region Lalala',
/* 4*/' // #region',
/* 5*/' x = 9;',
/* 6*/' // #endregion',
/* 7*/' // #endregion Lalala',
/* 8*/'</script>',
/* 9*/'</head>',
/*10*/'</html>',
];
await assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]);
});
test('Embedded CSS', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head>',
/* 2*/'<style>',
/* 3*/' foo {',
/* 4*/' display: block;',
/* 5*/' color: black;',
/* 6*/' }',
/* 7*/'</style>',
/* 8*/'</head>',
/* 9*/'</html>',
];
await assertRanges(input, [r(0, 8), r(1, 7), r(2, 6), r(3, 5)]);
});
test('Embedded CSS - multiple areas', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head style="color:red">',
/* 2*/'<style>',
/* 3*/' /*',
/* 4*/' foo: true,',
/* 5*/' bar: {}',
/* 6*/' */',
/* 7*/'</style>',
/* 8*/'<style>',
/* 9*/' @keyframes mymove {',
/*10*/' from {top: 0px;}',
/*11*/' }',
/*12*/'</style>',
/*13*/'</head>',
/*14*/'</html>',
];
await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6, 'comment'), r(8, 11), r(9, 10)]);
});
test('Embedded CSS - regions', async () => {
const input = [
/* 0*/'<html>',
/* 1*/'<head>',
/* 2*/'<style>',
/* 3*/' /* #region Lalala */',
/* 4*/' /* #region*/',
/* 5*/' x = 9;',
/* 6*/' /* #endregion*/',
/* 7*/' /* #endregion Lalala*/',
/* 8*/'</style>',
/* 9*/'</head>',
/*10*/'</html>',
];
await assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]);
});
// test('Embedded JavaScript - multi line comment', async () => {
// const input = [
// /* 0*/'<html>',
// /* 1*/'<head>',
// /* 2*/'<script>',
// /* 3*/' /*',
// /* 4*/' * Hello',
// /* 5*/' */',
// /* 6*/'</script>',
// /* 7*/'</head>',
// /* 8*/'</html>',
// ];
// await assertRanges(input, [r(0, 7), r(1, 6), r(2, 5), r(3, 5, 'comment')]);
// });
test('Test limit', async () => {
const input = [
/* 0*/'<div>',
/* 1*/' <span>',
/* 2*/' <b>',
/* 3*/' ',
/* 4*/' </b>,',
/* 5*/' <b>',
/* 6*/' <pre>',
/* 7*/' ',
/* 8*/' </pre>,',
/* 9*/' <pre>',
/*10*/' ',
/*11*/' </pre>,',
/*12*/' </b>,',
/*13*/' <b>',
/*14*/' ',
/*15*/' </b>,',
/*16*/' <b>',
/*17*/' ',
/*18*/' </b>',
/*19*/' </span>',
/*20*/'</div>',
];
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(9, 10), r(13, 14), r(16, 17)], 'no limit', undefined);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(9, 10), r(13, 14), r(16, 17)], 'limit 8', 8);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(13, 14), r(16, 17)], 'limit 7', 7);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14), r(16, 17)], 'limit 6', 6);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14)], 'limit 5', 5);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11)], 'limit 4', 4);
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3)], 'limit 3', 3);
await assertRanges(input, [r(0, 19), r(1, 18)], 'limit 2', 2);
await assertRanges(input, [r(0, 19)], 'limit 1', 1);
});
});

View File

@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as path from 'path';
import * as fs from 'fs';
import * as assert from 'assert';
import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes';
import { format } from '../modes/formatting';
import { getNodeFSRequestService } from '../node/nodeFs';
suite('HTML Embedded Formatting', () => {
async function assertFormat(value: string, expected: string, options?: any, formatOptions?: FormattingOptions, message?: string): Promise<void> {
let workspace = {
settings: options,
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
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));
if (!formatOptions) {
formatOptions = FormattingOptions.create(2, true);
}
let result = await format(languageModes, document, range, formatOptions, undefined, { css: true, javascript: true });
let actual = TextDocument.applyEdits(document, result);
assert.equal(actual, expected, message);
}
async function assertFormatWithFixture(fixtureName: string, expectedPath: string, options?: any, formatOptions?: FormattingOptions): Promise<void> {
let input = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'test', 'fixtures', 'inputs', fixtureName)).toString().replace(/\r\n/mg, '\n');
let expected = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'test', 'fixtures', 'expected', expectedPath)).toString().replace(/\r\n/mg, '\n');
await assertFormat(input, expected, options, formatOptions, expectedPath);
}
test('HTML only', async () => {
await assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
await assertFormat('|<html><body><p>Hello</p></body></html>|', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
await assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>');
});
test('HTML & Scripts', async () => {
await assertFormat('<html><head><script></script></head></html>', '<html>\n\n<head>\n <script></script>\n</head>\n\n</html>');
await 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>');
await assertFormat('<html><head><script>\nvar x=2;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 2;\n </script>\n</head>\n\n</html>');
await assertFormat('<html><head>\n <script>\nvar x=3;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 3;\n </script>\n</head>\n\n</html>');
await assertFormat('<html><head>\n <script>\nvar x=4;\nconsole.log("Hi");\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 4;\n console.log("Hi");\n </script>\n</head>\n\n</html>');
await assertFormat('<html><head>\n |<script>\nvar x=5;\n</script>|</head></html>', '<html><head>\n <script>\n var x = 5;\n </script></head></html>');
});
test('HTLM & Scripts - Fixtures', async () => {
assertFormatWithFixture('19813.html', '19813.html');
assertFormatWithFixture('19813.html', '19813-4spaces.html', undefined, FormattingOptions.create(4, true));
assertFormatWithFixture('19813.html', '19813-tab.html', undefined, FormattingOptions.create(1, false));
assertFormatWithFixture('21634.html', '21634.html');
});
test('Script end tag', async () => {
await assertFormat('<html>\n<head>\n <script>\nvar x = 0;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 0;\n </script>\n</head>\n\n</html>');
});
test('HTML & Multiple Scripts', async () => {
await 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', async () => {
await assertFormat('<html><head>\n<style>\n.foo{display:none;}\n</style></head></html>', '<html>\n\n<head>\n <style>\n .foo {\n display: none;\n }\n </style>\n</head>\n\n</html>');
});
test('EndWithNewline', async () => {
let options = {
html: {
format: {
endWithNewline: true
}
}
};
await assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>\n', options);
await assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>', options);
await 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);
});
test('Inside script', async () => {
await assertFormat('<html><head>\n <script>\n|var x=6;|\n</script></head></html>', '<html><head>\n <script>\n var x = 6;\n</script></head></html>');
await assertFormat('<html><head>\n <script>\n|var x=6;\nvar y= 9;|\n</script></head></html>', '<html><head>\n <script>\n var x = 6;\n var y = 9;\n</script></head></html>');
});
test('Range after new line', async () => {
await assertFormat('<html><head>\n |<script>\nvar x=6;\n</script>\n|</head></html>', '<html><head>\n <script>\n var x = 6;\n </script>\n</head></html>');
});
test('bug 36574', async () => {
await assertFormat('<script src="/js/main.js"> </script>', '<script src="/js/main.js"> </script>');
});
test('bug 48049', async () => {
await assertFormat(
[
'<html>',
'<head>',
'</head>',
'',
'<body>',
'',
' <script>',
' function f(x) {}',
' f(function () {',
' // ',
'',
' console.log(" vsc crashes on formatting")',
' });',
' </script>',
'',
'',
'',
' </body>',
'',
'</html>'
].join('\n'),
[
'<html>',
'',
'<head>',
'</head>',
'',
'<body>',
'',
' <script>',
' function f(x) {}',
' f(function () {',
' // ',
'',
' console.log(" vsc crashes on formatting")',
' });',
' </script>',
'',
'',
'',
'</body>',
'',
'</html>'
].join('\n')
);
});
test('#58435', async () => {
let options = {
html: {
format: {
contentUnformatted: 'textarea'
}
}
};
const content = [
'<html>',
'',
'<body>',
' <textarea name= "" id ="" cols="30" rows="10">',
' </textarea>',
'</body>',
'',
'</html>',
].join('\n');
const expected = [
'<html>',
'',
'<body>',
' <textarea name="" id="" cols="30" rows="10">',
' </textarea>',
'</body>',
'',
'</html>',
].join('\n');
await assertFormat(content, expected, options);
});
}); /*
content_unformatted: Array(4)["pre", "code", "textarea", …]
end_with_newline: false
eol: "\n"
extra_liners: Array(3)["head", "body", "/html"]
indent_char: "\t"
indent_handlebars: false
indent_inner_html: false
indent_size: 1
max_preserve_newlines: 32786
preserve_newlines: true
unformatted: Array(1)["wbr"]
wrap_attributes: "auto"
wrap_attributes_indent_size: undefined
wrap_line_length: 120*/

View File

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange} from '../modes/languageModes';
import { getSelectionRanges } from '../modes/selectionRanges';
import { getNodeFSRequestService } from '../node/nodeFs';
async function assertRanges(content: string, expected: (number | string)[][]): Promise<void> {
let message = `${content} gives selection range:\n`;
const offset = content.indexOf('|');
content = content.substr(0, offset) + content.substr(offset + 1);
let workspace = {
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const document = TextDocument.create('test://foo.html', 'html', 1, content);
const actualRanges = await getSelectionRanges(languageModes, document, [document.positionAt(offset)]);
assert.equal(actualRanges.length, 1);
const offsetPairs: [number, string][] = [];
let curr: SelectionRange | undefined = actualRanges[0];
while (curr) {
offsetPairs.push([document.offsetAt(curr.range.start), document.getText(curr.range)]);
curr = curr.parent;
}
message += `${JSON.stringify(offsetPairs)}\n but should give:\n${JSON.stringify(expected)}\n`;
assert.deepEqual(offsetPairs, expected, message);
}
suite('HTML SelectionRange', () => {
test('Embedded JavaScript', async () => {
await assertRanges('<html><head><script> function foo() { return ((1|+2)*6) }</script></head></html>', [
[48, '1'],
[48, '1+2'],
[47, '(1+2)'],
[47, '(1+2)*6'],
[46, '((1+2)*6)'],
[39, 'return ((1+2)*6)'],
[22, 'function foo() { return ((1+2)*6) }'],
[20, ' function foo() { return ((1+2)*6) }'],
[12, '<script> function foo() { return ((1+2)*6) }</script>'],
[6, '<head><script> function foo() { return ((1+2)*6) }</script></head>'],
[0, '<html><head><script> function foo() { return ((1+2)*6) }</script></head></html>'],
]);
});
test('Embedded CSS', async () => {
await assertRanges('<html><head><style>foo { display: |none; } </style></head></html>', [
[34, 'none'],
[25, 'display: none'],
[24, ' display: none; '],
[23, '{ display: none; }'],
[19, 'foo { display: none; }'],
[19, 'foo { display: none; } '],
[12, '<style>foo { display: none; } </style>'],
[6, '<head><style>foo { display: none; } </style></head>'],
[0, '<html><head><style>foo { display: none; } </style></head></html>'],
]);
});
test('Embedded style', async () => {
await assertRanges('<div style="color: |red"></div>', [
[19, 'red'],
[12, 'color: red'],
[11, '"color: red"'],
[5, 'style="color: red"'],
[1, 'div style="color: red"'],
[0, '<div style="color: red"></div>']
]);
});
});

View File

@ -0,0 +1,228 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { TextDocument, getLanguageModes, ClientCapabilities, Range, Position } from '../modes/languageModes';
import { newSemanticTokenProvider } from '../modes/semanticTokens';
import { getNodeFSRequestService } from '../node/nodeFs';
interface ExpectedToken {
startLine: number;
character: number;
length: number;
tokenClassifiction: string;
}
async function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: Range[], message?: string): Promise<void> {
const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n'));
const workspace = {
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const semanticTokensProvider = newSemanticTokenProvider(languageModes);
const legend = semanticTokensProvider.legend;
const actual = await semanticTokensProvider.getSemanticTokens(document, ranges);
let actualRanges = [];
let lastLine = 0;
let lastCharacter = 0;
for (let i = 0; i < actual.length; i += 5) {
const lineDelta = actual[i], charDelta = actual[i + 1], len = actual[i + 2], typeIdx = actual[i + 3], modSet = actual[i + 4];
const line = lastLine + lineDelta;
const character = lineDelta === 0 ? lastCharacter + charDelta : charDelta;
const tokenClassifiction = [legend.types[typeIdx], ...legend.modifiers.filter((_, i) => modSet & 1 << i)].join('.');
actualRanges.push(t(line, character, len, tokenClassifiction));
lastLine = line;
lastCharacter = character;
}
assert.deepEqual(actualRanges, expected, message);
}
function t(startLine: number, character: number, length: number, tokenClassifiction: string): ExpectedToken {
return { startLine, character, length, tokenClassifiction };
}
suite('HTML Semantic Tokens', () => {
test('Variables', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script>',
/*3*/' var x = 9, y1 = [x];',
/*4*/' try {',
/*5*/' for (const s of y1) { x = s }',
/*6*/' } catch (e) {',
/*7*/' throw y1;',
/*8*/' }',
/*9*/'</script>',
/*10*/'</head>',
/*11*/'</html>',
];
await assertTokens(input, [
t(3, 6, 1, 'variable.declaration'), t(3, 13, 2, 'variable.declaration'), t(3, 19, 1, 'variable'),
t(5, 15, 1, 'variable.declaration.readonly'), t(5, 20, 2, 'variable'), t(5, 26, 1, 'variable'), t(5, 30, 1, 'variable.readonly'),
t(6, 11, 1, 'variable.declaration'),
t(7, 10, 2, 'variable')
]);
});
test('Functions', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script>',
/*3*/' function foo(p1) {',
/*4*/' return foo(Math.abs(p1))',
/*5*/' }',
/*6*/' `/${window.location}`.split("/").forEach(s => foo(s));',
/*7*/'</script>',
/*8*/'</head>',
/*9*/'</html>',
];
await assertTokens(input, [
t(3, 11, 3, 'function.declaration'), t(3, 15, 2, 'parameter.declaration'),
t(4, 11, 3, 'function'), t(4, 15, 4, 'interface'), t(4, 20, 3, 'member'), t(4, 24, 2, 'parameter'),
t(6, 6, 6, 'variable'), t(6, 13, 8, 'property'), t(6, 24, 5, 'member'), t(6, 35, 7, 'member'), t(6, 43, 1, 'parameter.declaration'), t(6, 48, 3, 'function'), t(6, 52, 1, 'parameter')
]);
});
test('Members', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script>',
/*3*/' class A {',
/*4*/' static x = 9;',
/*5*/' f = 9;',
/*6*/' async m() { return A.x + await this.m(); };',
/*7*/' get s() { return this.f; ',
/*8*/' static t() { return new A().f; };',
/*9*/' constructor() {}',
/*10*/' }',
/*11*/'</script>',
/*12*/'</head>',
/*13*/'</html>',
];
await assertTokens(input, [
t(3, 8, 1, 'class.declaration'),
t(4, 11, 1, 'property.declaration.static'),
t(5, 4, 1, 'property.declaration'),
t(6, 10, 1, 'member.declaration.async'), t(6, 23, 1, 'class'), t(6, 25, 1, 'property.static'), t(6, 40, 1, 'member.async'),
t(7, 8, 1, 'property.declaration'), t(7, 26, 1, 'property'),
t(8, 11, 1, 'member.declaration.static'), t(8, 28, 1, 'class'), t(8, 32, 1, 'property'),
]);
});
test('Interfaces', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script type="text/typescript">',
/*3*/' interface Position { x: number, y: number };',
/*4*/' const p = { x: 1, y: 2 } as Position;',
/*5*/' const foo = (o: Position) => o.x + o.y;',
/*6*/'</script>',
/*7*/'</head>',
/*8*/'</html>',
];
await assertTokens(input, [
t(3, 12, 8, 'interface.declaration'), t(3, 23, 1, 'property.declaration'), t(3, 34, 1, 'property.declaration'),
t(4, 8, 1, 'variable.declaration.readonly'), t(4, 30, 8, 'interface'),
t(5, 8, 3, 'variable.declaration.readonly'), t(5, 15, 1, 'parameter.declaration'), t(5, 18, 8, 'interface'), t(5, 31, 1, 'parameter'), t(5, 33, 1, 'property'), t(5, 37, 1, 'parameter'), t(5, 39, 1, 'property')
]);
});
test('Readonly', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script type="text/typescript">',
/*3*/' const f = 9;',
/*4*/' class A { static readonly t = 9; static url: URL; }',
/*5*/' const enum E { A = 9, B = A + 1 }',
/*6*/' console.log(f + A.t + A.url.origin);',
/*7*/'</script>',
/*8*/'</head>',
/*9*/'</html>',
];
await assertTokens(input, [
t(3, 8, 1, 'variable.declaration.readonly'),
t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.static.readonly'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface'),
t(5, 13, 1, 'enum.declaration'), t(5, 17, 1, 'property.declaration.readonly'), t(5, 24, 1, 'property.declaration.readonly'), t(5, 28, 1, 'property.readonly'),
t(6, 2, 7, 'variable'), t(6, 10, 3, 'member'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.static.readonly'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly'),
]);
});
test('Type aliases and type parameters', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script type="text/typescript">',
/*3*/' type MyMap = Map<string, number>;',
/*4*/' function f<T extends MyMap>(t: T | number) : T { ',
/*5*/' return <T> <unknown> new Map<string, MyMap>();',
/*6*/' }',
/*7*/'</script>',
/*8*/'</head>',
/*9*/'</html>',
];
await assertTokens(input, [
t(3, 7, 5, 'type.declaration'), t(3, 15, 3, 'interface') /* to investiagte */,
t(4, 11, 1, 'function.declaration'), t(4, 13, 1, 'typeParameter.declaration'), t(4, 23, 5, 'type'), t(4, 30, 1, 'parameter.declaration'), t(4, 33, 1, 'typeParameter'), t(4, 47, 1, 'typeParameter'),
t(5, 12, 1, 'typeParameter'), t(5, 29, 3, 'interface'), t(5, 41, 5, 'type'),
]);
});
test('TS and JS', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script type="text/typescript">',
/*3*/' function f<T>(p1: T): T[] { return [ p1 ]; }',
/*4*/'</script>',
/*5*/'<script>',
/*6*/' window.alert("Hello");',
/*7*/'</script>',
/*8*/'</head>',
/*9*/'</html>',
];
await assertTokens(input, [
t(3, 11, 1, 'function.declaration'), t(3, 13, 1, 'typeParameter.declaration'), t(3, 16, 2, 'parameter.declaration'), t(3, 20, 1, 'typeParameter'), t(3, 24, 1, 'typeParameter'), t(3, 39, 2, 'parameter'),
t(6, 2, 6, 'variable'), t(6, 9, 5, 'member')
]);
});
test('Ranges', async () => {
const input = [
/*0*/'<html>',
/*1*/'<head>',
/*2*/'<script>',
/*3*/' window.alert("Hello");',
/*4*/'</script>',
/*5*/'<script>',
/*6*/' window.alert("World");',
/*7*/'</script>',
/*8*/'</head>',
/*9*/'</html>',
];
await assertTokens(input, [
t(3, 2, 6, 'variable'), t(3, 9, 5, 'member')
], [Range.create(Position.create(2, 0), Position.create(4, 0))]);
await assertTokens(input, [
t(6, 2, 6, 'variable'),
], [Range.create(Position.create(6, 2), Position.create(6, 8))]);
});
});

View File

@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as words from '../utils/strings';
suite('HTML Words', () => {
let wordRegex = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
function assertWord(value: string, expected: string): void {
let offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
let actualRange = words.getWordAtText(value, offset, wordRegex);
assert(actualRange.start <= offset);
assert(actualRange.start + actualRange.length >= offset);
assert.equal(value.substr(actualRange.start, actualRange.length), expected);
}
test('Basic', function (): any {
assertWord('|var x1 = new F<A>(a, b);', 'var');
assertWord('v|ar x1 = new F<A>(a, b);', 'var');
assertWord('var| x1 = new F<A>(a, b);', 'var');
assertWord('var |x1 = new F<A>(a, b);', 'x1');
assertWord('var x1| = new F<A>(a, b);', 'x1');
assertWord('var x1 = new |F<A>(a, b);', 'F');
assertWord('var x1 = new F<|A>(a, b);', 'A');
assertWord('var x1 = new F<A>(|a, b);', 'a');
assertWord('var x1 = new F<A>(a, b|);', 'b');
assertWord('var x1 = new F<A>(a, b)|;', '');
assertWord('var x1 = new F<A>(a, b)|;|', '');
assertWord('var x1 = | new F<A>(a, b)|;|', '');
});
test('Multiline', function (): any {
assertWord('console.log("hello");\n|var x1 = new F<A>(a, b);', 'var');
assertWord('console.log("hello");\n|\nvar x1 = new F<A>(a, b);', '');
assertWord('console.log("hello");\n\r |var x1 = new F<A>(a, b);', 'var');
});
});

View File

@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function pushAll<T>(to: T[], from: T[]) {
if (from) {
for (const e of from) {
to.push(e);
}
}
}
export function contains<T>(arr: T[], val: T) {
return arr.indexOf(val) !== -1;
}
/**
* Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort`
* so only use this when actually needing stable sort.
*/
export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] {
_divideAndMerge(data, compare);
return data;
}
function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void {
if (data.length <= 1) {
// sorted
return;
}
const p = (data.length / 2) | 0;
const left = data.slice(0, p);
const right = data.slice(p);
_divideAndMerge(left, compare);
_divideAndMerge(right, compare);
let leftIdx = 0;
let rightIdx = 0;
let i = 0;
while (leftIdx < left.length && rightIdx < right.length) {
let ret = compare(left[leftIdx], right[rightIdx]);
if (ret <= 0) {
// smaller_equal -> take left to preserve order
data[i++] = left[leftIdx++];
} else {
// greater -> take right
data[i++] = right[rightIdx++];
}
}
while (leftIdx < left.length) {
data[i++] = left[leftIdx++];
}
while (rightIdx < right.length) {
data[i++] = right[rightIdx++];
}
}
export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number {
let low = 0,
high = array.length - 1;
while (low <= high) {
let mid = ((low + high) / 2) | 0;
let comp = comparator(array[mid], key);
if (comp < 0) {
low = mid + 1;
} else if (comp > 0) {
high = mid - 1;
} else {
return mid;
}
}
return -(low + 1);
}

View File

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DocumentContext } from 'vscode-css-languageservice';
import { endsWith, startsWith } from '../utils/strings';
import { WorkspaceFolder } from 'vscode-languageserver';
import { resolvePath } from '../requests';
export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext {
function getRootFolder(): string | undefined {
for (let folder of workspaceFolders) {
let folderURI = folder.uri;
if (!endsWith(folderURI, '/')) {
folderURI = folderURI + '/';
}
if (startsWith(documentUri, folderURI)) {
return folderURI;
}
}
return undefined;
}
return {
resolveReference: (ref: string, base = documentUri) => {
if (ref[0] === '/') { // resolve absolute path against the current workspace folder
let folderUri = getRootFolder();
if (folderUri) {
return folderUri + ref.substr(1);
}
}
base = base.substr(0, base.lastIndexOf('/') + 1);
return resolvePath(base, ref);
},
};
}

View File

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Position, Range } from '../modes/languageModes';
export function beforeOrSame(p1: Position, p2: Position) {
return p1.line < p2.line || p1.line === p2.line && p1.character <= p2.character;
}
export function insideRangeButNotSame(r1: Range, r2: Range) {
return beforeOrSame(r1.start, r2.start) && beforeOrSame(r2.end, r1.end) && !equalRange(r1, r2);
}
export function equalRange(r1: Range, r2: Range) {
return r1.start.line === r2.start.line && r1.start.character === r2.start.character && r1.end.line === r2.end.line && r1.end.character === r2.end.character;
}

View File

@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResponseError, ErrorCodes, CancellationToken } from 'vscode-languageserver';
export function formatError(message: string, err: any): string {
if (err instanceof Error) {
let error = <Error>err;
return `${message}: ${error.message}\n${error.stack}`;
} else if (typeof err === 'string') {
return `${message}: ${err}`;
} else if (err) {
return `${message}: ${err.toString()}`;
}
return message;
}
export function runSafe<T>(func: () => Thenable<T>, errorVal: T, errorMessage: string, token: CancellationToken): Thenable<T | ResponseError<any>> {
return new Promise<T | ResponseError<any>>((resolve) => {
setImmediate(() => {
if (token.isCancellationRequested) {
resolve(cancelValue());
}
return func().then(result => {
if (token.isCancellationRequested) {
resolve(cancelValue());
return;
} else {
resolve(result);
}
}, e => {
console.error(formatError(errorMessage, e));
resolve(errorVal);
});
});
});
}
function cancelValue<E>() {
return new ResponseError<E>(ErrorCodes.RequestCancelled, 'Request cancelled');
}

View File

@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number, length: number } {
let lineStart = offset;
while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) {
lineStart--;
}
let offsetInLine = offset - lineStart;
let lineText = text.substr(lineStart);
// make a copy of the regex as to not keep the state
let flags = wordDefinition.ignoreCase ? 'gi' : 'g';
wordDefinition = new RegExp(wordDefinition.source, flags);
let match = wordDefinition.exec(lineText);
while (match && match.index + match[0].length < offsetInLine) {
match = wordDefinition.exec(lineText);
}
if (match && match.index <= offsetInLine) {
return { start: match.index + lineStart, length: match[0].length };
}
return { start: offset, length: 0 };
}
export function startsWith(haystack: string, needle: string): boolean {
if (haystack.length < needle.length) {
return false;
}
for (let i = 0; i < needle.length; i++) {
if (haystack[i] !== needle[i]) {
return false;
}
}
return true;
}
export function endsWith(haystack: string, needle: string): boolean {
let diff = haystack.length - needle.length;
if (diff > 0) {
return haystack.indexOf(needle, diff) === diff;
} else if (diff === 0) {
return haystack === needle;
} else {
return false;
}
}
export function repeat(value: string, count: number) {
let s = '';
while (count > 0) {
if ((count & 1) === 1) {
s += value;
}
value += value;
count = count >>> 1;
}
return s;
}
export function isWhitespaceOnly(str: string) {
return /^\s*$/.test(str);
}
export function isEOL(content: string, offset: number) {
return isNewlineCharacter(content.charCodeAt(offset));
}
const CR = '\r'.charCodeAt(0);
const NL = '\n'.charCodeAt(0);
export function isNewlineCharacter(charCode: number) {
return charCode === CR || charCode === NL;
}

View File

@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const Mocha = require('mocha');
const glob = require('glob');
const suite = 'Integration HTML Extension Tests';
const options = {
ui: 'tdd',
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
timeout: 60000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
const mocha = new Mocha(options);
glob.sync(__dirname + '/../out/test/**/*.test.js')
.forEach(file => mocha.addFile(file));
mocha.run(failures => process.exit(failures ? -1 : 0));

View File

@ -0,0 +1,9 @@
{
"extends": "../../shared.tsconfig.json",
"compilerOptions": {
"outDir": "./out"
},
"include": [
"src/**/*"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@^12.11.7":
version "12.11.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a"
integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA==
applicationinsights@1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5"
integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg==
dependencies:
diagnostic-channel "0.2.0"
diagnostic-channel-publishers "0.2.1"
zone.js "0.7.6"
diagnostic-channel-publishers@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
diagnostic-channel@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
dependencies:
semver "^5.3.0"
semver@^5.3.0:
version "5.5.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
vscode-extension-telemetry@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b"
integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA==
dependencies:
applicationinsights "1.0.8"
vscode-jsonrpc@6.0.0-next.2:
version "6.0.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f"
integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw==
vscode-languageclient@7.0.0-next.5.1:
version "7.0.0-next.5.1"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0-next.5.1.tgz#ed93f14e4c2cdccedf15002c7bf8ef9cb638f36c"
integrity sha512-OONvbk3IFpubwF8/Y5uPQaq5J5CEskpeET3SfK4iGlv5OUK+44JawH/SEW5wXuEPpfdMLEMZLuGLU5v5d7N7PQ==
dependencies:
semver "^6.3.0"
vscode-languageserver-protocol "3.16.0-next.4"
vscode-languageserver-protocol@3.16.0-next.4:
version "3.16.0-next.4"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f"
integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ==
dependencies:
vscode-jsonrpc "6.0.0-next.2"
vscode-languageserver-types "3.16.0-next.2"
vscode-languageserver-types@3.16.0-next.2:
version "3.16.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083"
integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==
vscode-nls@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
zone.js@0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=