Initial release

This commit is contained in:
Martin Aeschlimann 2016-09-15 19:33:23 +02:00
parent 08e7666e5c
commit f26f3636d9
17 changed files with 1417 additions and 2 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/node_modules/
/out/
/release/

8
.npmignore Normal file
View file

@ -0,0 +1,8 @@
/.vscode/
/lib/
/out/
/src/
/test/
/release/dev/
/gulpfile.js
/.npmignore

9
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,9 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.trimTrailingWhitespace": true,
"search.exclude": {
"**/node_modules": true,
"**/release": true,
"**/out": true
}
}

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,2 +1,48 @@
# monaco-html
HTML language plugin for the Monaco Editor.
# Monaco HTML
HTML language plugin for the Monaco Editor. It provides the following features when editing HTML files:
* Code completion
* Formatting
* Document Highlights
* Link detection
* Syntax highlighting
Internally the HTML plugin uses the [vscode-html-languageservice](https://github.com/Microsoft/vscode-html-languageservice)
node module, providing the implementation of the functionally listed above. The same module is also used
in [Visual Studio Code](https://github.com/Microsoft/vscode) to power the HTML editing experience.
## Issues
Please file issues concering `monaco-html` in the [`monaco-editor`-repository](https://github.com/Microsoft/monaco-editor/issues).
## Installing
This npm module is bundled and distributed in the [monaco-editor](https://www.npmjs.com/package/monaco-editor) npm module.
* change to your favorite source folder (`/src/`)
* `git clone https://github.com/Microsoft/monaco-editor` (this will create `$/src/monaco-editor`)
* in folder `monaco-editor` run `npm install` and run `npm run simpleserver`
* open http://localhost:8080/monaco-editor/test/index.html#sample - html
## Development
### Dev: Running monaco-html from source
* change to your favorite source folder (`/src/`).
* if you haven't done so: `git clone https://github.com/Microsoft/monaco-editor` (this will create `$/src/monaco-editor`)
* `git clone https://github.com/Microsoft/monaco-html` (this will create `$/src/monaco-html`)
* Important: both monaco repositories must have the same parent folder.
* in folder `monaco-html` run `npm install` and run `npm run watch`
* in folder `monaco-editor` run `npm install` and run `npm run simpleserver`
* open http://localhost:8080/monaco-editor/test/?monaco-html=dev
### [Optional] Running monaco-editor-core from source
* this is only needed when you want to make changes also in `monaco-editor-core`.
* change to the same favorite source folder (`/src/`) that already contains `monaco-html` and `monaco-editor`
* `git clone https://github.com/Microsoft/vscode` (this will create `$/src/vscode/`)
* read [here](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#installing-prerequisites) on how to initialize the VS Code source repository.
* in folder `vscode` run `gulp watch`
* open http://localhost:8080/monaco-editor/test/?monaco-html=dev&editor=dev
## License
[MIT](https://github.com/Microsoft/monaco-html/blob/master/LICENSE.md)

246
gulpfile.js Normal file
View file

@ -0,0 +1,246 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var gulp = require('gulp');
var tsb = require('gulp-tsb');
var assign = require('object-assign');
var fs = require('fs');
var path = require('path');
var merge = require('merge-stream');
var rjs = require('gulp-requirejs');
var uglify = require('gulp-uglify');
var rimraf = require('rimraf');
var es = require('event-stream');
gulp.task('clean-release', function(cb) { rimraf('release', { maxBusyTries: 1 }, cb); });
gulp.task('release', ['clean-release','compile'], function() {
var sha1 = getGitVersion(__dirname);
var semver = require('./package.json').version;
var headerVersion = semver + '(' + sha1 + ')';
var BUNDLED_FILE_HEADER = [
'/*!-----------------------------------------------------------------------------',
' * Copyright (c) Microsoft Corporation. All rights reserved.',
' * monaco-html version: ' + headerVersion,
' * Released under the MIT license',
' * https://github.com/Microsoft/monaco-html/blob/master/LICENSE.md',
' *-----------------------------------------------------------------------------*/',
''
].join('\n');
function getDependencyLocation(name, libLocation, container) {
var location = __dirname + '/node_modules/' + name + '/' + libLocation;
if (!fs.existsSync(location)) {
var oldLocation = __dirname + '/node_modules/' + container + '/node_modules/' + name + '/' + libLocation;
if (!fs.existsSync(oldLocation)) {
console.error('Unable to find ' + name + ' node module at ' + location + ' or ' + oldLocation);
return;
}
return oldLocation;
}
return location;
}
var uriLocation = getDependencyLocation('vscode-uri', 'lib', 'vscode-html-languageservice');
function bundleOne(moduleId, exclude) {
return rjs({
baseUrl: '/out/',
name: 'vs/language/html/' + moduleId,
out: moduleId + '.js',
exclude: exclude,
paths: {
'vs/language/html': __dirname + '/out'
},
packages: [{
name: 'vscode-html-languageservice/lib/parser/htmlScanner',
location: __dirname + '/node_modules/vscode-html-languageservice/lib/parser',
main: 'htmlScanner'
}, {
name: 'vscode-html-languageservice',
location: __dirname + '/node_modules/vscode-html-languageservice/lib',
main: 'htmlLanguageService'
}, {
name: 'vscode-languageserver-types',
location: __dirname + '/node_modules/vscode-languageserver-types/lib',
main: 'main'
}, {
name: 'vscode-uri',
location: uriLocation,
main: 'index'
}, {
name: 'vscode-nls',
location: __dirname + '/out/fillers',
main: 'vscode-nls'
}]
})
}
return merge(
merge(
bundleOne('monaco.contribution', ['vs/language/html/htmlMode']),
bundleOne('htmlMode'),
bundleOne('htmlWorker')
)
.pipe(es.through(function(data) {
data.contents = new Buffer(
BUNDLED_FILE_HEADER
+ data.contents.toString()
);
this.emit('data', data);
}))
.pipe(gulp.dest('./release/dev'))
.pipe(uglify({
preserveComments: 'some'
}))
.pipe(gulp.dest('./release/min')),
gulp.src('src/monaco.d.ts').pipe(gulp.dest('./release/min'))
);
});
var compilation = tsb.create(assign({ verbose: true }, require('./src/tsconfig.json').compilerOptions));
var tsSources = 'src/**/*.ts';
function compileTask() {
return merge(
gulp.src(tsSources).pipe(compilation())
)
.pipe(gulp.dest('out'));
}
gulp.task('clean-out', function(cb) { rimraf('out', { maxBusyTries: 1 }, cb); });
gulp.task('compile', ['clean-out'], compileTask);
gulp.task('compile-without-clean', compileTask);
gulp.task('watch', ['compile'], function() {
gulp.watch(tsSources, ['compile-without-clean']);
});
/**
* Escape text such that it can be used in a javascript string enclosed by double quotes (")
*/
function escapeText(text) {
// http://www.javascriptkit.com/jsref/escapesequence.shtml
// \b Backspace.
// \f Form feed.
// \n Newline.
// \O Nul character.
// \r Carriage return.
// \t Horizontal tab.
// \v Vertical tab.
// \' Single quote or apostrophe.
// \" Double quote.
// \\ Backslash.
// \ddd The Latin-1 character specified by the three octal digits between 0 and 377. ie, copyright symbol is \251.
// \xdd The Latin-1 character specified by the two hexadecimal digits dd between 00 and FF. ie, copyright symbol is \xA9.
// \udddd The Unicode character specified by the four hexadecimal digits dddd. ie, copyright symbol is \u00A9.
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('');
}
function getGitVersion(repo) {
var git = path.join(repo, '.git');
var headPath = path.join(git, 'HEAD');
var head;
try {
head = fs.readFileSync(headPath, 'utf8').trim();
} catch (e) {
return void 0;
}
if (/^[0-9a-f]{40}$/i.test(head)) {
return head;
}
var refMatch = /^ref: (.*)$/.exec(head);
if (!refMatch) {
return void 0;
}
var ref = refMatch[1];
var refPath = path.join(git, ref);
try {
return fs.readFileSync(refPath, 'utf8').trim();
} catch (e) {
// noop
}
var packedRefsPath = path.join(git, 'packed-refs');
var refsRaw;
try {
refsRaw = fs.readFileSync(packedRefsPath, 'utf8').trim();
} catch (e) {
return void 0;
}
var refsRegex = /^([0-9a-f]{40})\s+(.+)$/gm;
var refsMatch;
var refs = {};
while (refsMatch = refsRegex.exec(refsRaw)) {
refs[refsMatch[2]] = refsMatch[1];
}
return refs[ref];
}

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "monaco-html",
"version": "1.0.0-next.1",
"description": "HTML plugin for the Monaco Editor",
"scripts": {
"compile": "gulp compile",
"watch": "gulp watch",
"prepublish": "gulp release",
"install-service-next": "npm install vscode-html-languageservice@next -f -D",
"install-service-local": "npm install ../vscode-html-languageservice -f -D"
},
"author": "Microsoft Corporation",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/monaco-html"
},
"bugs": {
"url": "https://github.com/Microsoft/monaco-editor/issues"
},
"devDependencies": {
"event-stream": "^3.3.2",
"gulp": "^3.9.1",
"gulp-requirejs": "^0.1.3",
"gulp-tsb": "^1.10.4",
"gulp-uglify": "^1.5.3",
"merge-stream": "^1.0.0",
"monaco-editor-core": "0.6.0",
"object-assign": "^4.1.0",
"rimraf": "^2.5.2",
"typescript": "1.8.10",
"vscode-html-languageservice": "^1.0.0-next.2",
"vscode-languageserver-types": "^1.0.4-next.2"
}
}

34
src/fillers/vscode-nls.ts Normal file
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.
*--------------------------------------------------------------------------------------------*/
import {LocalizeInfo, LocalizeFunc, Options, LoadFunc} from 'vscode-nls';
export {LocalizeInfo, LocalizeFunc, Options, LoadFunc};
function format(message: string, args: any[]): string {
let result:string;
if (args.length === 0) {
result = message;
} else {
result = message.replace(/\{(\d+)\}/g, (match, rest) => {
let index = rest[0];
return typeof args[index] !== 'undefined' ? args[index] : match;
});
}
return result;
}
function localize(key: string | LocalizeInfo, message: string, ...args: any[]): string {
return format(message, args);
}
export function loadMessageBundle(file?: string): LocalizeFunc {
return localize;
}
export function config(opt?: Options | string): LoadFunc {
return loadMessageBundle;
}

79
src/htmlMode.ts Normal file
View file

@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {WorkerManager} from './workerManager';
import {HTMLWorker} from './htmlWorker';
import {LanguageServiceDefaultsImpl} from './monaco.contribution';
import * as languageFeatures from './languageFeatures';
import {createTokenizationSupport} from './tokenization';
import Promise = monaco.Promise;
import Uri = monaco.Uri;
import IDisposable = monaco.IDisposable;
export function setupMode(defaults: LanguageServiceDefaultsImpl): void {
let disposables: IDisposable[] = [];
const client = new WorkerManager(defaults);
disposables.push(client);
const worker: languageFeatures.WorkerAccessor = (...uris: Uri[]): Promise<HTMLWorker> => {
return client.getLanguageServiceWorker(...uris);
};
let languageId = defaults.languageId;
disposables.push(monaco.languages.registerCompletionItemProvider(languageId, new languageFeatures.CompletionAdapter(worker)));
disposables.push(monaco.languages.registerDocumentHighlightProvider(languageId, new languageFeatures.DocumentHighlightAdapter(worker)));
disposables.push(monaco.languages.registerDocumentFormattingEditProvider(languageId, new languageFeatures.DocumentFormattingEditProvider(worker)));
disposables.push(monaco.languages.registerDocumentRangeFormattingEditProvider(languageId, new languageFeatures.DocumentRangeFormattingEditProvider(worker)));
disposables.push(new languageFeatures.DiagnostcsAdapter(languageId, worker));
disposables.push(monaco.languages.registerLinkProvider(languageId, new languageFeatures.DocumentLinkAdapter(worker)));
disposables.push(monaco.languages.setTokensProvider(languageId, createTokenizationSupport(true)));
disposables.push(monaco.languages.setLanguageConfiguration(languageId, richEditConfiguration));
}
const EMPTY_ELEMENTS:string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
const richEditConfiguration: monaco.languages.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\[\{\]\}\:\"\,\s]+)/g,
comments: {
blockComment: ['<!--', '-->']
},
brackets: [
['<!--', '-->'],
['<', '>'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: '\'', close: '\'' }
],
surroundingPairs: [
{ open: '"', close: '"' },
{ open: '\'', close: '\'' }
],
onEnterRules: [
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: monaco.languages.IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: monaco.languages.IndentAction.Indent }
}
],
};

73
src/htmlWorker.ts Normal file
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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import IWorkerContext = monaco.worker.IWorkerContext;
import Thenable = monaco.Thenable;
import Promise = monaco.Promise;
import * as htmlService from 'vscode-html-languageservice';
import * as ls from 'vscode-languageserver-types';
export class HTMLWorker {
private _ctx:IWorkerContext;
private _languageService: htmlService.LanguageService;
private _languageSettings: monaco.languages.html.Options;
private _languageId: string;
constructor(ctx:IWorkerContext, createData: ICreateData) {
this._ctx = ctx;
this._languageSettings = createData.languageSettings;
this._languageId = createData.languageId;
this._languageService = htmlService.getLanguageService();
}
doValidation(uri: string): Thenable<ls.Diagnostic[]> {
// not yet suported
return Promise.as([]);
}
doComplete(uri: string, position: ls.Position): Thenable<ls.CompletionList> {
let document = this._getTextDocument(uri);
let htmlDocument = this._languageService.parseHTMLDocument(document);
return Promise.as(this._languageService.doComplete(document, position, htmlDocument));
}
format(uri: string, range: ls.Range, options: ls.FormattingOptions): Thenable<ls.TextEdit[]> {
let document = this._getTextDocument(uri);
let textEdits = this._languageService.format(document, range, this._languageSettings && this._languageSettings.format);
return Promise.as(textEdits);
}
findDocumentHighlights(uri: string, position: ls.Position): Promise<ls.DocumentHighlight[]> {
let document = this._getTextDocument(uri);
let htmlDocument = this._languageService.parseHTMLDocument(document);
let highlights = this._languageService.findDocumentHighlights(document, position, htmlDocument);
return Promise.as(highlights);
}
findDocumentLinks(uri: string, workspacePath: string): Promise<ls.DocumentLink[]> {
let document = this._getTextDocument(uri);
let links = this._languageService.findDocumentLinks(document, workspacePath);
return Promise.as(links);
}
private _getTextDocument(uri: string): ls.TextDocument {
let models = this._ctx.getMirrorModels();
for (let model of models) {
if (model.uri.toString() === uri) {
return ls.TextDocument.create(uri, this._languageId, model.version, model.getValue());
}
}
return null;
}
}
export interface ICreateData {
languageId: string;
languageSettings: monaco.languages.html.Options;
}
export function create(ctx:IWorkerContext, createData: ICreateData): HTMLWorker {
return new HTMLWorker(ctx, createData);
}

440
src/languageFeatures.ts Normal file
View file

@ -0,0 +1,440 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {LanguageServiceDefaultsImpl} from './monaco.contribution';
import {HTMLWorker} from './htmlWorker';
import * as ls from 'vscode-languageserver-types';
import Uri = monaco.Uri;
import Position = monaco.Position;
import Range = monaco.Range;
import Thenable = monaco.Thenable;
import Promise = monaco.Promise;
import CancellationToken = monaco.CancellationToken;
import IDisposable = monaco.IDisposable;
export interface WorkerAccessor {
(...more: Uri[]): Thenable<HTMLWorker>
}
// --- diagnostics --- ---
export class DiagnostcsAdapter {
private _disposables: IDisposable[] = [];
private _listener: { [uri: string]: IDisposable } = Object.create(null);
constructor(private _languageId: string, private _worker: WorkerAccessor) {
const onModelAdd = (model: monaco.editor.IModel): void => {
let modeId = model.getModeId();
if (modeId !== this._languageId) {
return;
}
let handle: number;
this._listener[model.uri.toString()] = model.onDidChangeContent(() => {
clearTimeout(handle);
handle = setTimeout(() => this._doValidate(model.uri, modeId), 500);
});
this._doValidate(model.uri, modeId);
};
const onModelRemoved = (model: monaco.editor.IModel): void => {
monaco.editor.setModelMarkers(model, this._languageId, []);
delete this._listener[model.uri.toString()];
};
this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd));
this._disposables.push(monaco.editor.onWillDisposeModel(model => {
onModelRemoved(model);
}));
this._disposables.push(monaco.editor.onDidChangeModelLanguage(event => {
onModelRemoved(event.model);
onModelAdd(event.model);
}));
this._disposables.push({
dispose: () => {
for (let key in this._listener) {
this._listener[key].dispose();
}
}
});
monaco.editor.getModels().forEach(onModelAdd);
}
public dispose(): void {
this._disposables.forEach(d => d && d.dispose());
this._disposables = [];
}
private _doValidate(resource: Uri, languageId: string): void {
this._worker(resource).then(worker => {
return worker.doValidation(resource.toString()).then(diagnostics => {
const markers = diagnostics.map(d => toDiagnostics(resource, d));
monaco.editor.setModelMarkers(monaco.editor.getModel(resource), languageId, markers);
});
}).then(undefined, err => {
console.error(err);
});
}
}
function toSeverity(lsSeverity: number): monaco.Severity {
switch (lsSeverity) {
case ls.DiagnosticSeverity.Error: return monaco.Severity.Error;
case ls.DiagnosticSeverity.Warning: return monaco.Severity.Warning;
case ls.DiagnosticSeverity.Information:
case ls.DiagnosticSeverity.Hint:
default:
return monaco.Severity.Info;
}
}
function toDiagnostics(resource: Uri, diag: ls.Diagnostic): monaco.editor.IMarkerData {
let code = typeof diag.code === 'number' ? String(diag.code) : <string>diag.code;
return {
severity: toSeverity(diag.severity),
startLineNumber: diag.range.start.line + 1,
startColumn: diag.range.start.character + 1,
endLineNumber: diag.range.end.line + 1,
endColumn: diag.range.end.character + 1,
message: diag.message,
code: code,
source: diag.source
};
}
// --- completion ------
function fromPosition(position: Position): ls.Position {
if (!position) {
return void 0;
}
return { character: position.column - 1, line: position.lineNumber - 1 };
}
function fromRange(range: Range): ls.Range {
if (!range) {
return void 0;
}
return { start: fromPosition(range.getStartPosition()), end: fromPosition(range.getEndPosition()) };
}
function toRange(range: ls.Range): Range {
if (!range) {
return void 0;
}
return new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);
}
function toCompletionItemKind(kind: number): monaco.languages.CompletionItemKind {
let mItemKind = monaco.languages.CompletionItemKind;
switch (kind) {
case ls.CompletionItemKind.Text: return mItemKind.Text;
case ls.CompletionItemKind.Method: return mItemKind.Method;
case ls.CompletionItemKind.Function: return mItemKind.Function;
case ls.CompletionItemKind.Constructor: return mItemKind.Constructor;
case ls.CompletionItemKind.Field: return mItemKind.Field;
case ls.CompletionItemKind.Variable: return mItemKind.Variable;
case ls.CompletionItemKind.Class: return mItemKind.Class;
case ls.CompletionItemKind.Interface: return mItemKind.Interface;
case ls.CompletionItemKind.Module: return mItemKind.Module;
case ls.CompletionItemKind.Property: return mItemKind.Property;
case ls.CompletionItemKind.Unit: return mItemKind.Unit;
case ls.CompletionItemKind.Value: return mItemKind.Value;
case ls.CompletionItemKind.Enum: return mItemKind.Enum;
case ls.CompletionItemKind.Keyword: return mItemKind.Keyword;
case ls.CompletionItemKind.Snippet: return mItemKind.Snippet;
case ls.CompletionItemKind.Color: return mItemKind.Color;
case ls.CompletionItemKind.File: return mItemKind.File;
case ls.CompletionItemKind.Reference: return mItemKind.Reference;
}
return mItemKind.Property;
}
function fromCompletionItemKind(kind: monaco.languages.CompletionItemKind): ls.CompletionItemKind {
let mItemKind = monaco.languages.CompletionItemKind;
switch (kind) {
case mItemKind.Text: return ls.CompletionItemKind.Text;
case mItemKind.Method: return ls.CompletionItemKind.Method;
case mItemKind.Function: return ls.CompletionItemKind.Function;
case mItemKind.Constructor: return ls.CompletionItemKind.Constructor;
case mItemKind.Field: return ls.CompletionItemKind.Field;
case mItemKind.Variable: return ls.CompletionItemKind.Variable;
case mItemKind.Class: return ls.CompletionItemKind.Class;
case mItemKind.Interface: return ls.CompletionItemKind.Interface;
case mItemKind.Module: return ls.CompletionItemKind.Module;
case mItemKind.Property: return ls.CompletionItemKind.Property;
case mItemKind.Unit: return ls.CompletionItemKind.Unit;
case mItemKind.Value: return ls.CompletionItemKind.Value;
case mItemKind.Enum: return ls.CompletionItemKind.Enum;
case mItemKind.Keyword: return ls.CompletionItemKind.Keyword;
case mItemKind.Snippet: return ls.CompletionItemKind.Snippet;
case mItemKind.Color: return ls.CompletionItemKind.Color;
case mItemKind.File: return ls.CompletionItemKind.File;
case mItemKind.Reference: return ls.CompletionItemKind.Reference;
}
return ls.CompletionItemKind.Property;
}
function toTextEdit(textEdit: ls.TextEdit): monaco.editor.ISingleEditOperation {
if (!textEdit) {
return void 0;
}
return {
range: toRange(textEdit.range),
text: textEdit.newText
}
}
function fromTextEdit(editOp: monaco.editor.ISingleEditOperation): ls.TextEdit {
if (!editOp) {
return void 0;
}
return {
range: fromRange(Range.lift(editOp.range)),
newText: editOp.text
}
}
interface DataCompletionItem extends monaco.languages.CompletionItem {
data?: any;
}
function toCompletionItem(entry: ls.CompletionItem): DataCompletionItem {
return {
label: entry.label,
insertText: entry.insertText,
sortText: entry.sortText,
filterText: entry.filterText,
documentation: entry.documentation,
detail: entry.detail,
kind: toCompletionItemKind(entry.kind),
textEdit: toTextEdit(entry.textEdit),
data: entry.data
};
}
function fromCompletionItem(entry: DataCompletionItem): ls.CompletionItem {
return {
label: entry.label,
insertText: entry.insertText,
sortText: entry.sortText,
filterText: entry.filterText,
documentation: entry.documentation,
detail: entry.detail,
kind: fromCompletionItemKind(entry.kind),
textEdit: fromTextEdit(entry.textEdit),
data: entry.data
};
}
export class CompletionAdapter implements monaco.languages.CompletionItemProvider {
constructor(private _worker: WorkerAccessor) {
}
public get triggerCharacters(): string[] {
return ['.', ':', '<', '"', '=', '/'];
}
provideCompletionItems(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable<monaco.languages.CompletionList> {
const wordInfo = model.getWordUntilPosition(position);
const resource = model.uri;
return wireCancellationToken(token, this._worker(resource).then(worker => {
return worker.doComplete(resource.toString(), fromPosition(position)).then(info => {
if (!info) {
return;
}
let items: monaco.languages.CompletionItem[] = info.items.map(entry => {
return {
label: entry.label,
insertText: entry.insertText,
sortText: entry.sortText,
filterText: entry.filterText,
documentation: entry.documentation,
detail: entry.detail,
kind: toCompletionItemKind(entry.kind),
textEdit: toTextEdit(entry.textEdit)
};
});
return <monaco.languages.CompletionList>{
isIncomplete: info.isIncomplete,
items: items
};
});
}));
}
}
function toMarkedStringArray(contents: ls.MarkedString | ls.MarkedString[]): monaco.MarkedString[] {
if (!contents) {
return void 0;
}
if (Array.isArray(contents)) {
return (<ls.MarkedString[]>contents);
}
return [<ls.MarkedString>contents];
}
// --- definition ------
function toLocation(location: ls.Location): monaco.languages.Location {
return {
uri: Uri.parse(location.uri),
range: toRange(location.range)
};
}
// --- document symbols ------
function toSymbolKind(kind: ls.SymbolKind): monaco.languages.SymbolKind {
let mKind = monaco.languages.SymbolKind;
switch (kind) {
case ls.SymbolKind.File: return mKind.Array;
case ls.SymbolKind.Module: return mKind.Module;
case ls.SymbolKind.Namespace: return mKind.Namespace;
case ls.SymbolKind.Package: return mKind.Package;
case ls.SymbolKind.Class: return mKind.Class;
case ls.SymbolKind.Method: return mKind.Method;
case ls.SymbolKind.Property: return mKind.Property;
case ls.SymbolKind.Field: return mKind.Field;
case ls.SymbolKind.Constructor: return mKind.Constructor;
case ls.SymbolKind.Enum: return mKind.Enum;
case ls.SymbolKind.Interface: return mKind.Interface;
case ls.SymbolKind.Function: return mKind.Function;
case ls.SymbolKind.Variable: return mKind.Variable;
case ls.SymbolKind.Constant: return mKind.Constant;
case ls.SymbolKind.String: return mKind.String;
case ls.SymbolKind.Number: return mKind.Number;
case ls.SymbolKind.Boolean: return mKind.Boolean;
case ls.SymbolKind.Array: return mKind.Array;
}
return mKind.Function;
}
function toHighlighKind(kind: ls.DocumentHighlightKind): monaco.languages.DocumentHighlightKind {
let mKind = monaco.languages.DocumentHighlightKind;
switch (kind) {
case ls.DocumentHighlightKind.Read: return mKind.Read;
case ls.DocumentHighlightKind.Write: return mKind.Write;
case ls.DocumentHighlightKind.Text: return mKind.Text;
}
return mKind.Text;
}
export class DocumentHighlightAdapter implements monaco.languages.DocumentHighlightProvider {
constructor(private _worker: WorkerAccessor) {
}
public provideDocumentHighlights(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable<monaco.languages.DocumentHighlight[]> {
const resource = model.uri;
return wireCancellationToken(token, this._worker(resource).then(worker => worker.findDocumentHighlights(resource.toString(), fromPosition(position))).then(items => {
if (!items) {
return;
}
return items.map(item => ({
range: toRange(item.range),
kind: toHighlighKind(item.kind)
}));
}));
}
}
export class DocumentLinkAdapter implements monaco.languages.LinkProvider {
constructor(private _worker: WorkerAccessor) {
}
public provideLinks(model: monaco.editor.IReadOnlyModel, token: CancellationToken): Thenable<monaco.languages.ILink[]> {
const resource = model.uri;
return wireCancellationToken(token, this._worker(resource).then(worker => worker.findDocumentLinks(resource.toString(), null)).then(items => {
if (!items) {
return;
}
return items.map(item => ({
range: toRange(item.range),
url: item.target
}));
}));
}
}
function fromFormattingOptions(options: monaco.languages.FormattingOptions): ls.FormattingOptions {
return {
tabSize: options.tabSize,
insertSpaces: options.insertSpaces
};
}
export class DocumentFormattingEditProvider implements monaco.languages.DocumentFormattingEditProvider {
constructor(private _worker: WorkerAccessor) {
}
public provideDocumentFormattingEdits(model: monaco.editor.IReadOnlyModel, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable<monaco.editor.ISingleEditOperation[]> {
const resource = model.uri;
return wireCancellationToken(token, this._worker(resource).then(worker => {
return worker.format(resource.toString(), null, fromFormattingOptions(options)).then(edits => {
if (!edits || edits.length === 0) {
return;
}
return edits.map(toTextEdit);
});
}));
}
}
export class DocumentRangeFormattingEditProvider implements monaco.languages.DocumentRangeFormattingEditProvider {
constructor(private _worker: WorkerAccessor) {
}
public provideDocumentRangeFormattingEdits(model: monaco.editor.IReadOnlyModel, range: Range, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable<monaco.editor.ISingleEditOperation[]> {
const resource = model.uri;
return wireCancellationToken(token, this._worker(resource).then(worker => {
return worker.format(resource.toString(), fromRange(range), fromFormattingOptions(options)).then(edits => {
if (!edits || edits.length === 0) {
return;
}
return edits.map(toTextEdit);
});
}));
}
}
/**
* Hook a cancellation token to a WinJS Promise
*/
function wireCancellationToken<T>(token: CancellationToken, promise: Thenable<T>): Thenable<T> {
if ((<Promise<T>>promise).cancel) {
token.onCancellationRequested(() => (<Promise<T>>promise).cancel());
}
return promise;
}

124
src/monaco.contribution.ts Normal file
View file

@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as mode from './htmlMode';
import Emitter = monaco.Emitter;
import IEvent = monaco.IEvent;
import IDisposable = monaco.IDisposable;
declare var require: <T>(moduleId: [string], callback: (module: T) => void) => void;
// --- HTML configuration and defaults ---------
export class LanguageServiceDefaultsImpl implements monaco.languages.html.LanguageServiceDefaults {
private _onDidChange = new Emitter<monaco.languages.html.LanguageServiceDefaults>();
private _options: monaco.languages.html.Options;
private _languageId: string;
constructor(languageId: string, options: monaco.languages.html.Options) {
this._languageId = languageId;
this.setOptions(options);
}
get onDidChange(): IEvent<monaco.languages.html.LanguageServiceDefaults> {
return this._onDidChange.event;
}
get languageId(): string {
return this._languageId;
}
get options(): monaco.languages.html.Options {
return this._options;
}
setOptions(options: monaco.languages.html.Options): void {
this._options = options || Object.create(null);
this._onDidChange.fire(this);
}
}
const formatDefaults: monaco.languages.html.HTMLFormatConfiguration = {
tabSize: 4,
insertSpaces: false,
wrapLineLength: 120,
unformatted: 'a, abbr, acronym, b, bdo, big, br, button, cite, code, dfn, em, i, img, input, kbd, label, map, object, q, samp, script, select, small, span, strong, sub, sup, textarea, tt, var',
indentInnerHtml: false,
preserveNewLines: true,
maxPreserveNewLines: null,
indentHandlebars: false,
endWithNewline: false,
extraLiners: 'head, body, /html'
};
const htmlOptionsDefault: monaco.languages.html.Options = {
format: formatDefaults,
suggest: { html5: true, angular1: true, ionic: true }
}
const handlebarOptionsDefault: monaco.languages.html.Options = {
format: formatDefaults,
suggest: { html5: true }
}
const razorOptionsDefault: monaco.languages.html.Options = {
format: formatDefaults,
suggest: { html5: true, razor: true }
}
const htmlLanguageId = 'html';
const handlebarsLanguageId = 'handlebars';
const razorLanguageId = 'razor';
const htmlDefaults = new LanguageServiceDefaultsImpl(htmlLanguageId, htmlOptionsDefault);
const handlebarDefaults = new LanguageServiceDefaultsImpl(handlebarsLanguageId, handlebarOptionsDefault);
const razorDefaults = new LanguageServiceDefaultsImpl(razorLanguageId, razorOptionsDefault);
// Export API
function createAPI(): typeof monaco.languages.html {
return {
htmlDefaults: htmlDefaults,
razorDefaults: handlebarDefaults,
handlebarDefaults: razorDefaults
}
}
monaco.languages.html = createAPI();
// --- Registration to monaco editor ---
function withMode(callback: (module: typeof mode) => void): void {
require<typeof mode>(['vs/language/html/htmlMode'], callback);
}
monaco.languages.register({
id: htmlLanguageId,
extensions: ['.html', '.htm', '.shtml', '.xhtml', '.mdoc', '.jsp', '.asp', '.aspx', '.jshtm'],
aliases: ['HTML', 'htm', 'html', 'xhtml'],
mimetypes: ['text/html', 'text/x-jshtm', 'text/template', 'text/ng-template']
});
monaco.languages.register({
id: handlebarsLanguageId,
extensions: ['.handlebars', '.hbs'],
aliases: ['Handlebars', 'handlebars'],
mimetypes: ['text/x-handlebars-template']
});
monaco.languages.register({
id: razorLanguageId,
extensions: ['.cshtml'],
aliases: ['Razor', 'razor'],
mimetypes: ['text/x-cshtml']
});
monaco.languages.onLanguage(htmlLanguageId, () => {
withMode(mode => mode.setupMode(htmlDefaults));
});
monaco.languages.onLanguage(handlebarsLanguageId, () => {
withMode(mode => mode.setupMode(handlebarDefaults));
});
monaco.languages.onLanguage(razorLanguageId, () => {
withMode(mode => mode.setupMode(razorDefaults));
});

44
src/monaco.d.ts vendored Normal file
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.
*--------------------------------------------------------------------------------------------*/
declare module monaco.languages.html {
export interface HTMLFormatConfiguration {
tabSize: number;
insertSpaces: boolean;
wrapLineLength: number;
unformatted: string;
indentInnerHtml: boolean;
preserveNewLines: boolean;
maxPreserveNewLines: number;
indentHandlebars: boolean;
endWithNewline: boolean;
extraLiners: string;
}
export interface CompletionConfiguration {
[provider: string]: boolean;
}
export interface Options {
/**
* If set, comments are tolerated. If set to false, syntax errors will be emmited for comments.
*/
format?: HTMLFormatConfiguration;
/**
* A list of known schemas and/or associations of schemas to file names.
*/
suggest?: CompletionConfiguration;
}
export interface LanguageServiceDefaults {
onDidChange: IEvent<LanguageServiceDefaults>;
options: Options;
setOptions(options: Options): void;
}
export var htmlDefaults: LanguageServiceDefaults;
export var handlebarDefaults: LanguageServiceDefaults;
export var razorDefaults: LanguageServiceDefaults;
}

136
src/tokenization.ts Normal file
View file

@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {Scanner, ScannerState, TokenType, createScanner} from 'vscode-html-languageservice/lib/parser/htmlScanner';
export function createTokenizationSupport(supportComments: boolean): monaco.languages.TokensProvider {
return {
getInitialState: () => new HTMLState(null, ScannerState.WithinContent),
tokenize: (line, state, offsetDelta?, stopAtOffset?) => tokenize(line, <HTMLState>state, offsetDelta, stopAtOffset)
};
}
const DELIM_END = 'punctuation.definition.meta.tag.end.html';
const DELIM_START = 'punctuation.definition.meta.tag.begin.html';
const DELIM_ASSIGN = 'meta.tag.assign.html';
const ATTRIB_NAME = 'entity.other.attribute-name.html';
const ATTRIB_VALUE = 'string.html';
const COMMENT = 'comment.html.content';
const DELIM_COMMENT = 'comment.html';
const DOCTYPE = 'entity.other.attribute-name.html';
const DELIM_DOCTYPE = 'entity.name.tag.html';
function getTag(name: string) {
return TAG_PREFIX + name;
}
const TAG_PREFIX = 'entity.name.tag.tag-';
class HTMLState implements monaco.languages.IState {
private _state: monaco.languages.IState;
public scannerState: ScannerState;
constructor(state: monaco.languages.IState, scannerState: ScannerState) {
this._state = state;
this.scannerState = scannerState;
}
public clone(): HTMLState {
return new HTMLState(this._state, this.scannerState);
}
public equals(other: monaco.languages.IState): boolean {
if (other === this) {
return true;
}
if (!other || !(other instanceof HTMLState)) {
return false;
}
return this.scannerState === (<HTMLState>other).scannerState;
}
public getStateData(): monaco.languages.IState {
return this._state;
}
public setStateData(state: monaco.languages.IState): void {
this._state = state;
}
}
function tokenize(line: string, state: HTMLState, offsetDelta: number = 0, stopAtOffset = line.length): monaco.languages.ILineTokens {
let scanner = createScanner(line, 0, state && state.scannerState);
let tokenType = scanner.scan();
let ret = {
tokens: <monaco.languages.IToken[]>[],
endState: state.clone()
};
let position = -1;
while (tokenType !== TokenType.EOS && scanner.getTokenOffset() < stopAtOffset) {
let scope;
switch (tokenType) {
case TokenType.AttributeName:
scope = ATTRIB_NAME;
break;
case TokenType.AttributeValue:
scope = ATTRIB_VALUE;
break;
case TokenType.StartTag:
case TokenType.EndTag:
scope = getTag(scanner.getTokenText());
break;
case TokenType.DelimiterAssign:
scope = DELIM_ASSIGN;
break;
case TokenType.StartTagOpen:
case TokenType.StartTagClose:
case TokenType.StartTagSelfClose:
scope = DELIM_START;
break;
case TokenType.EndTagOpen:
case TokenType.EndTagClose:
scope = DELIM_END;
break;
case TokenType.Doctype:
scope = DOCTYPE;
break;
case TokenType.StartDoctypeTag:
case TokenType.EndDoctypeTag:
scope = DELIM_DOCTYPE;
break;
case TokenType.Comment:
scope = COMMENT;
break;
case TokenType.StartCommentTag:
case TokenType.EndCommentTag:
scope = DELIM_COMMENT;
break;
default:
scope = '';
break;
}
if (position < scanner.getTokenOffset()) {
ret.tokens.push({
startIndex: scanner.getTokenOffset() + offsetDelta,
scopes: scope
});
} else {
throw new Error('Scanner did not advance, next 3 characters are: ' + line.substr(scanner.getTokenOffset(), 3));
}
position = scanner.getTokenOffset();
tokenType = scanner.scan();
}
ret.endState = new HTMLState(state.getStateData(), scanner.getScannerState());
return ret;
}

8
src/tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "umd",
"moduleResolution": "node",
"outDir": "../out",
"target": "es5"
}
}

5
src/typings/refs.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../node_modules/monaco-editor-core/monaco.d.ts'/>

104
src/workerManager.ts Normal file
View file

@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {LanguageServiceDefaultsImpl} from './monaco.contribution';
import {HTMLWorker} from './htmlWorker';
import Promise = monaco.Promise;
import IDisposable = monaco.IDisposable;
import Uri = monaco.Uri;
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1000; // 2min
export class WorkerManager {
private _defaults: LanguageServiceDefaultsImpl;
private _idleCheckInterval: number;
private _lastUsedTime: number;
private _configChangeListener: IDisposable;
private _worker: monaco.editor.MonacoWebWorker<HTMLWorker>;
private _client: Promise<HTMLWorker>;
constructor(defaults: LanguageServiceDefaultsImpl) {
this._defaults = defaults;
this._worker = null;
this._idleCheckInterval = setInterval(() => this._checkIfIdle(), 30 * 1000);
this._lastUsedTime = 0;
this._configChangeListener = this._defaults.onDidChange(() => this._stopWorker());
}
private _stopWorker(): void {
if (this._worker) {
this._worker.dispose();
this._worker = null;
}
this._client = null;
}
dispose(): void {
clearInterval(this._idleCheckInterval);
this._configChangeListener.dispose();
this._stopWorker();
}
private _checkIfIdle(): void {
if (!this._worker) {
return;
}
let timePassedSinceLastUsed = Date.now() - this._lastUsedTime;
if (timePassedSinceLastUsed > STOP_WHEN_IDLE_FOR) {
this._stopWorker();
}
}
private _getClient(): Promise<HTMLWorker> {
this._lastUsedTime = Date.now();
if (!this._client) {
this._worker = monaco.editor.createWebWorker<HTMLWorker>({
// module that exports the create() method and returns a `HTMLWorker` instance
moduleId: 'vs/language/html/htmlWorker',
// passed in to the create() method
createData: {
languageSettings: this._defaults.options,
languageId: this._defaults.languageId
}
});
this._client = this._worker.getProxy();
}
return this._client;
}
getLanguageServiceWorker(...resources: Uri[]): Promise<HTMLWorker> {
let _client: HTMLWorker;
return toShallowCancelPromise(
this._getClient().then((client) => {
_client = client
}).then(_ => {
return this._worker.withSyncedResources(resources)
}).then(_ => _client)
);
}
}
function toShallowCancelPromise<T>(p: Promise<T>): Promise<T> {
let completeCallback: (value: T) => void;
let errorCallback: (err: any) => void;
let r = new Promise<T>((c, e) => {
completeCallback = c;
errorCallback = e;
}, () => { });
p.then(completeCallback, errorCallback);
return r;
}