From 2380ce142c46103d615683c91f2220e791925d04 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Wed, 22 Jun 2016 16:03:03 +0200 Subject: [PATCH] Init --- .gitignore | 3 + .npmignore | 8 + .vscode/settings.json | 9 + LICENSE.md | 21 ++ README.md | 15 ++ gulpfile.js | 210 ++++++++++++++++++ package.json | 36 +++ src/languageFeatures.ts | 440 +++++++++++++++++++++++++++++++++++++ src/mode.ts | 120 ++++++++++ src/monaco.contribution.ts | 113 ++++++++++ src/monaco.d.ts | 39 ++++ src/tsconfig.json | 8 + src/typings/refs.d.ts | 5 + src/worker.ts | 119 ++++++++++ src/workerManager.ts | 103 +++++++++ 15 files changed, 1249 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/languageFeatures.ts create mode 100644 src/mode.ts create mode 100644 src/monaco.contribution.ts create mode 100644 src/monaco.d.ts create mode 100644 src/tsconfig.json create mode 100644 src/typings/refs.d.ts create mode 100644 src/worker.ts create mode 100644 src/workerManager.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d9ef910d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/out/ +/release/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..1adf861c --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +/.vscode/ +/lib/ +/out/ +/src/ +/test/ +/gulpfile.js +/tsconfig.json +/.npmignore diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1dc255b6 --- /dev/null +++ b/.vscode/settings.json @@ -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 + } +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..f8a94f6e --- /dev/null +++ b/LICENSE.md @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index d3ad1ba1..bb9e1156 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ +<<<<<<< dc598757e71a1ee33423bd24c3cb1b4b849722e6 # monaco-css CSS language support for the Monaco editor +======= +# Monaco TypeScript + +TypeScript and JavaScript language support for the Monaco Editor. + +![typescript](https://cloud.githubusercontent.com/assets/5047891/15926623/5262fe08-2e3d-11e6-9b90-1d43fda07178.gif) + +## Installing + +This npm module is bundled and distributed in the [monaco-editor](https://www.npmjs.com/package/monaco-editor) npm module. + +## License +[MIT](https://github.com/Microsoft/monaco-typescript/blob/master/LICENSE.md) +>>>>>>> Init diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..753a5bca --- /dev/null +++ b/gulpfile.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +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-typescript version: ' + headerVersion, + ' * Released under the MIT license', + ' * https://github.com/Microsoft/monaco-typescript/blob/master/LICENSE.md', + ' *-----------------------------------------------------------------------------*/', + '' + ].join('\n'); + + function bundleOne(moduleId, exclude) { + return rjs({ + baseUrl: '/out/', + name: 'vs/language/css/' + moduleId, + out: moduleId + '.js', + exclude: exclude, + paths: { + 'vs/language/css': __dirname + '/out', + 'vscode-css-languageservice/lib': __dirname + '/node_modules/vscode-css-languageservice/lib', + 'vscode-languageserver-types/lib': __dirname + '/node_modules/vscode-languageserver-types/lib' + } + }) + } + + return merge( + merge( + bundleOne('monaco.contribution'), + bundleOne('mode'), + bundleOne('worker') + ) + .pipe(uglify({ + preserveComments: 'some' + })) + .pipe(es.through(function(data) { + data.contents = new Buffer( + BUNDLED_FILE_HEADER + + data.contents.toString() + ); + this.emit('data', data); + })) + .pipe(gulp.dest('./release/')), + + gulp.src('src/monaco.d.ts').pipe(gulp.dest('./release/')) + ); +}); + + +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]; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..4565387c --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "monaco-css", + "version": "0.0.1", + "description": "CSS, LESS and SCSS support for Monaco Editor", + "scripts": { + "test": "node_modules/.bin/mocha", + "watch": "node_modules/.bin/gulp watch", + "prepublish": "node_modules/.bin/gulp release" + }, + "author": "Microsoft Corporation", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/monaco-css" + }, + "bugs": { + "url": "https://github.com/Microsoft/monaco-css/issues" + }, + "dependencies": { + "vscode-css-languageservice": "file:../vscode-css-languageservice", + "vscode-languageserver-types": "1.0.0" + }, + "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", + "mocha": "^2.5.3", + "monaco-editor-core": "^0.4.0", + "object-assign": "^4.1.0", + "rimraf": "^2.5.2", + "typescript": "1.8.10" + } +} diff --git a/src/languageFeatures.ts b/src/languageFeatures.ts new file mode 100644 index 00000000..6e1fe4a6 --- /dev/null +++ b/src/languageFeatures.ts @@ -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 {CSSWorker} from './worker'; + +import * as cssService from 'vscode-css-languageservice/lib/cssLanguageService'; +import * as ls from 'vscode-languageserver-types/lib/main'; + + +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; + + +// --- diagnostics --- --- + +export class DiagnostcsAdapter { + + private _disposables: IDisposable[] = []; + private _listener: { [uri: string]: IDisposable } = Object.create(null); + + constructor(private _defaults: LanguageServiceDefaultsImpl, private _languageId: string, + private _worker: (first: Uri, ...more: Uri[]) => Promise + ) { + + 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 => { + delete this._listener[model.uri.toString()]; + }; + + this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd)); + this._disposables.push(monaco.editor.onWillDisposeModel(onModelRemoved)); + 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); + }).done(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) : 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 + }; +} + +// --- suggest ------ + +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 lsItemKind = ls.CompletionItemKind; + let mItemKind = monaco.languages.CompletionItemKind; + + switch (kind) { + case lsItemKind.Text: return mItemKind.Text; + case lsItemKind.Method: return mItemKind.Method; + case lsItemKind.Function: return mItemKind.Function; + case lsItemKind.Constructor: return mItemKind.Constructor; + case lsItemKind.Field: return mItemKind.Field; + case lsItemKind.Variable: return mItemKind.Variable; + case lsItemKind.Class: return mItemKind.Class; + case lsItemKind.Interface: return mItemKind.Interface; + case lsItemKind.Module: return mItemKind.Module; + case lsItemKind.Property: return mItemKind.Property; + case lsItemKind.Unit: return mItemKind.Unit; + case lsItemKind.Value: return mItemKind.Value; + case lsItemKind.Enum: return mItemKind.Enum; + case lsItemKind.Keyword: return mItemKind.Keyword; + case lsItemKind.Snippet: return mItemKind.Snippet; + case lsItemKind.Color: return mItemKind.Color; + case lsItemKind.File: return mItemKind.File; + case lsItemKind.Reference: return mItemKind.Reference; + } + return mItemKind.Property; +} + +function toTextEdit(textEdit: ls.TextEdit): monaco.editor.ISingleEditOperation { + if (!textEdit) { + return void 0; + } + return { + range: toRange(textEdit.range), + text: textEdit.newText + } +} + +export class CompletionAdapter implements monaco.languages.CompletionItemProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + public get triggerCharacters(): string[] { + return [' ', ':']; + } + + provideCompletionItems(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { + 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.insertText, + sortText: entry.sortText, + filterText: entry.filterText, + documentation: entry.documentation, + detail: entry.detail, + kind: toCompletionItemKind(entry.kind), + textEdit: toTextEdit(entry.textEdit) + }; + }); + + return { + isIncomplete: info.isIncomplete, + items: items + }; + })); + } +} + +function toHTMLContentElements(contents: ls.MarkedString | ls.MarkedString[]): monaco.IHTMLContentElement[] { + if (!contents) { + return void 0; + } + let toHTMLContentElement = (ms: ls.MarkedString): monaco.IHTMLContentElement => { + if (typeof ms === 'string') { + return { text: ms }; + } + return { + code: { + value: ms['value'], + language: ms['language'] + } + }; + }; + + if (Array.isArray(contents)) { + return (contents).map(toHTMLContentElement); + } + return [toHTMLContentElement(contents)]; +} + + +// --- hover ------ + +export class HoverAdapter implements monaco.languages.HoverProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + provideHover(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { + let resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => { + return worker.doHover(resource.toString(), fromPosition(position)); + }).then(info => { + if (!info) { + return; + } + return { + range: toRange(info.range), + htmlContent: toHTMLContentElements(info.contents) + }; + })); + } +} + +// --- occurrences ------ + +function toDocumentHighlightKind(kind: number): monaco.languages.DocumentHighlightKind { + switch (kind) { + case ls.DocumentHighlightKind.Read: return monaco.languages.DocumentHighlightKind.Read; + case ls.DocumentHighlightKind.Write: return monaco.languages.DocumentHighlightKind.Write; + case ls.DocumentHighlightKind.Text: return monaco.languages.DocumentHighlightKind.Text; + } + return monaco.languages.DocumentHighlightKind.Text; +} + + +export class DocumentHighlightAdapter implements monaco.languages.DocumentHighlightProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + public provideDocumentHighlights(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { + const resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => { + return worker.findDocumentHighlights(resource.toString(), fromPosition(position)) + }).then(entries => { + if (!entries) { + return; + } + return entries.map(entry => { + return { + range: toRange(entry.range), + kind: toDocumentHighlightKind(entry.kind) + }; + }); + })); + } +} + +// --- definition ------ + +function toLocation(location: ls.Location): monaco.languages.Location { + return { + uri: Uri.parse(location.uri), + range: toRange(location.range) + }; +} + +export class DefinitionAdapter { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + public provideDefinition(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { + const resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => { + return worker.findDefinition(resource.toString(), fromPosition(position)); + }).then(definition => { + if (!definition) { + return; + } + return [toLocation(definition)]; + })); + } +} + +// --- references ------ + +export class ReferenceAdapter implements monaco.languages.ReferenceProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + provideReferences(model: monaco.editor.IReadOnlyModel, position: Position, context: monaco.languages.ReferenceContext, token: CancellationToken): Thenable { + const resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => { + return worker.findReferences(resource.toString(), fromPosition(position)); + }).then(entries => { + if (!entries) { + return; + } + return entries.map(toLocation); + })); + } +} + +// --- rename ------ + +function toWorkspaceEdit(edit: ls.WorkspaceEdit): monaco.languages.WorkspaceEdit { + if (!edit || !edit.changes) { + return void 0; + } + let resourceEdits : monaco.languages.IResourceEdit[] = []; + for (let uri in edit.changes) { + let edits = edit.changes[uri]; + for (let e of edits) { + resourceEdits.push({resource: Uri.parse(uri), range: toRange(e.range), newText: e.newText }); + } + + } + return { + edits: resourceEdits + } +} + + +export class RenameAdapter implements monaco.languages.RenameProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + provideRenameEdits(model: monaco.editor.IReadOnlyModel, position: Position, newName: string, token: CancellationToken): Thenable { + const resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => { + return worker.doRename(resource.toString(), fromPosition(position), newName); + }).then(edit => { + return toWorkspaceEdit(edit); + })); + } +} + +// --- outline ------ + +function toSymbolKind(kind: ls.SymbolKind): monaco.languages.SymbolKind { + let lsKind = ls.SymbolKind; + let mKind = monaco.languages.SymbolKind; + + switch (kind) { + case lsKind.File: return mKind.Array; + case lsKind.Module: return mKind.Module; + case lsKind.Namespace: return mKind.Namespace; + case lsKind.Package: return mKind.Package; + case lsKind.Class: return mKind.Class; + case lsKind.Method: return mKind.Method; + case lsKind.Property: return mKind.Property; + case lsKind.Field: return mKind.Field; + case lsKind.Constructor: return mKind.Constructor; + case lsKind.Enum: return mKind.Enum; + case lsKind.Interface: return mKind.Interface; + case lsKind.Function: return mKind.Function; + case lsKind.Variable: return mKind.Variable; + case lsKind.Constant: return mKind.Constant; + case lsKind.String: return mKind.String; + case lsKind.Number: return mKind.Number; + case lsKind.Boolean: return mKind.Boolean; + case lsKind.Array: return mKind.Array; + } + return mKind.Function; +} + + +export class DocumentSymbolAdapter implements monaco.languages.DocumentSymbolProvider { + + constructor(private _worker: (first: Uri, ...more: Uri[]) => Promise) { + } + + public provideDocumentSymbols(model: monaco.editor.IReadOnlyModel, token: CancellationToken): Thenable { + const resource = model.uri; + + return wireCancellationToken(token, this._worker(resource).then(worker => worker.findDocumentSymbols(resource.toString())).then(items => { + if (!items) { + return; + } + return items.map(item => ({ + name: item.name, + containerName: item.containerName, + kind: toSymbolKind(item.kind), + location: toLocation(item.location) + })); + })); + } +} + +/** + * Hook a cancellation token to a WinJS Promise + */ +function wireCancellationToken(token: CancellationToken, promise: Promise): Thenable { + token.onCancellationRequested(() => promise.cancel()); + return promise; +} diff --git a/src/mode.ts b/src/mode.ts new file mode 100644 index 00000000..3ee26df8 --- /dev/null +++ b/src/mode.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {CSSWorker} from './worker'; +import {LanguageServiceDefaultsImpl} from './monaco.contribution'; +import * as languageFeatures from './languageFeatures'; + +import Promise = monaco.Promise; +import Uri = monaco.Uri; +import IDisposable = monaco.IDisposable; + +export function setupCSS(defaults:LanguageServiceDefaultsImpl): void { + const cssLanguageConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g, + + comments: { + blockComment: ['/*', '*/'] + }, + + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '\'', close: '\'', notIn: ['string'] } + ] + }; + + setupMode( + defaults, + 'css', + cssLanguageConfiguration + ); +} + +export function setupLESS(defaults:LanguageServiceDefaultsImpl): void { + const lessLanguageConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /(#?-?\d*\.\d\w*%?)|([@#!.:]?[\w-?]+%?)|[@#!.]/g, + comments: { + blockComment: ['/*', '*/'], + lineComment: '//' + }, + brackets: [['{','}'], ['[',']'], ['(',')'], ['<','>']], + autoClosingPairs: [ + { open: '"', close: '"', notIn: ['string', 'comment'] }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '{', close: '}', notIn: ['string', 'comment'] }, + { open: '[', close: ']', notIn: ['string', 'comment'] }, + { open: '(', close: ')', notIn: ['string', 'comment'] }, + { open: '<', close: '>', notIn: ['string', 'comment'] }, + ] + }; + + setupMode( + defaults, + 'less', + lessLanguageConfiguration + ); +} + +export function setupSCSS(defaults:LanguageServiceDefaultsImpl): void { + const scssLanguageConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /(#?-?\d*\.\d\w*%?)|([@#!.:]?[\w-?]+%?)|[@#!.]/g, + comments: { + blockComment: ['/*', '*/'], + lineComment: '//' + }, + brackets: [['{','}'], ['[',']'], ['(',')'], ['<','>']], + autoClosingPairs: [ + { open: '"', close: '"', notIn: ['string', 'comment'] }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] }, + { open: '{', close: '}', notIn: ['string', 'comment'] }, + { open: '[', close: ']', notIn: ['string', 'comment'] }, + { open: '(', close: ')', notIn: ['string', 'comment'] }, + { open: '<', close: '>', notIn: ['string', 'comment'] }, + ] + }; + + setupMode( + defaults, + 'scss', + scssLanguageConfiguration + ); +} + +function setupMode(defaults:LanguageServiceDefaultsImpl, modeId:string, languageConfiguration: monaco.languages.LanguageConfiguration): void { + + let disposables: IDisposable[] = []; + + const client = new WorkerManager(defaults); + disposables.push(client); + + const worker = (first: Uri, ...more: Uri[]): Promise => { + return client.getLanguageServiceWorker(...[first].concat(more)); + }; + + disposables.push(monaco.languages.registerCompletionItemProvider(modeId, new languageFeatures.CompletionAdapter(worker))); + disposables.push(monaco.languages.registerHoverProvider(modeId, new languageFeatures.HoverAdapter(worker))); + disposables.push(monaco.languages.registerDocumentHighlightProvider(modeId, new languageFeatures.DocumentHighlightAdapter(worker))); + disposables.push(monaco.languages.registerDefinitionProvider(modeId, new languageFeatures.DefinitionAdapter(worker))); + disposables.push(monaco.languages.registerReferenceProvider(modeId, new languageFeatures.ReferenceAdapter(worker))); + disposables.push(monaco.languages.registerDocumentSymbolProvider(modeId, new languageFeatures.DocumentSymbolAdapter(worker))); + disposables.push(monaco.languages.registerRenameProvider(modeId, new languageFeatures.RenameAdapter(worker))); + disposables.push(new languageFeatures.DiagnostcsAdapter(defaults, modeId, worker)); + disposables.push(monaco.languages.setLanguageConfiguration(modeId, languageConfiguration)); +} + + + + diff --git a/src/monaco.contribution.ts b/src/monaco.contribution.ts new file mode 100644 index 00000000..ec0842c7 --- /dev/null +++ b/src/monaco.contribution.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './mode'; + +import Emitter = monaco.Emitter; +import IEvent = monaco.IEvent; +import IDisposable = monaco.IDisposable; + +declare var require:(moduleId:[string], callback:(module:T)=>void)=>void; + +// --- CSS configuration and defaults --------- + +export class LanguageServiceDefaultsImpl implements monaco.languages.css.LanguageServiceDefaults { + + private _onDidChange = new Emitter(); + private _diagnosticsOptions: monaco.languages.css.DiagnosticsOptions; + + constructor(diagnosticsOptions: monaco.languages.css.DiagnosticsOptions) { + this.setDiagnosticsOptions(diagnosticsOptions); + } + + get onDidChange(): IEvent{ + return this._onDidChange.event; + } + + get diagnosticsOptions(): monaco.languages.css.DiagnosticsOptions { + return this._diagnosticsOptions; + } + + setDiagnosticsOptions(options: monaco.languages.css.DiagnosticsOptions): void { + this._diagnosticsOptions = options || Object.create(null); + this._onDidChange.fire(this); + } +} + +const diagnosticDefault : monaco.languages.css.DiagnosticsOptions = { + validate: true, + lint: { + compatibleVendorPrefixes: 'ignore', + vendorPrefix: 'warning', + duplicateProperties: 'warning', + emptyRules: 'warning', + importStatement: 'ignore', + boxModel: 'ignore', + universalSelector: 'ignore', + zeroUnits: 'ignore', + fontFaceProperties: 'warning', + hexColorLength: 'error', + argumentsInColorFunction: 'error', + unknownProperties: 'warning', + ieHack: 'ignore', + unknownVendorSpecificProperties: 'ignore', + propertyIgnoredDueToDisplay: 'warning', + important: 'ignore', + float: 'ignore', + idSelector: 'ignore' + } +} + +const cssDefaults = new LanguageServiceDefaultsImpl(diagnosticDefault); +const scssDefaults = new LanguageServiceDefaultsImpl(diagnosticDefault); +const lessDefaults = new LanguageServiceDefaultsImpl(diagnosticDefault); + + +// Export API +function createAPI(): typeof monaco.languages.css { + return { + cssDefaults: cssDefaults, + lessDefaults: lessDefaults, + scssDefaults: scssDefaults + } +} +monaco.languages.css = createAPI(); + +// --- Registration to monaco editor --- + +function withMode(callback:(module:typeof mode)=>void): void { + require(['vs/language/css/mode'], callback); +} + +monaco.languages.register({ + id: 'less', + extensions: ['.less'], + aliases: ['Less', 'less'], + mimetypes: ['text/x-less', 'text/less'] +}); +monaco.languages.onLanguage('less', () => { + withMode((mode) => mode.setupLESS(lessDefaults)); +}); + +monaco.languages.register({ + id: 'scss', + extensions: ['.scss'], + aliases: ['Sass', 'sass', 'scss'], + mimetypes: ['text/x-scss', 'text/scss'] +}); +monaco.languages.onLanguage('scss', () => { + withMode((mode) => mode.setupSCSS(scssDefaults)); +}); + +monaco.languages.register({ + id: 'css', + extensions: ['.css'], + aliases: ['CSS', 'css'], + mimetypes: ['text/css'] +}); +monaco.languages.onLanguage('css', () => { + withMode((mode) => mode.setupCSS(cssDefaults)); +}); diff --git a/src/monaco.d.ts b/src/monaco.d.ts new file mode 100644 index 00000000..5948681e --- /dev/null +++ b/src/monaco.d.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ +declare module monaco.languages.css { + export interface DiagnosticsOptions { + validate?: boolean; + lint?: { + compatibleVendorPrefixes?: 'ignore' | 'warning' | 'error', + vendorPrefix?: 'ignore' | 'warning' | 'error', + duplicateProperties?: 'ignore' | 'warning' | 'error', + emptyRules?: 'ignore' | 'warning' | 'error', + importStatement?: 'ignore' | 'warning' | 'error', + boxModel?: 'ignore' | 'warning' | 'error', + universalSelector?: 'ignore' | 'warning' | 'error', + zeroUnits?: 'ignore' | 'warning' | 'error', + fontFaceProperties?: 'ignore' | 'warning' | 'error', + hexColorLength?: 'ignore' | 'warning' | 'error', + argumentsInColorFunction?: 'ignore' | 'warning' | 'error', + unknownProperties?: 'ignore' | 'warning' | 'error', + ieHack?: 'ignore' | 'warning' | 'error', + unknownVendorSpecificProperties?: 'ignore' | 'warning' | 'error', + propertyIgnoredDueToDisplay?: 'ignore' | 'warning' | 'error', + important?: 'ignore' | 'warning' | 'error', + float?: 'ignore' | 'warning' | 'error', + idSelector?: 'ignore' | 'warning' | 'error' + } + } + + export interface LanguageServiceDefaults { + onDidChange: IEvent; + diagnosticsOptions: DiagnosticsOptions; + setDiagnosticsOptions(options: DiagnosticsOptions): void; + } + + export var cssDefaults: LanguageServiceDefaults; + export var lessDefaults: LanguageServiceDefaults; + export var scssDefaults: LanguageServiceDefaults; +} \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 00000000..6bd5e44b --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "umd", + "moduleResolution": "node", + "outDir": "../out", + "target": "es5" + } +} \ No newline at end of file diff --git a/src/typings/refs.d.ts b/src/typings/refs.d.ts new file mode 100644 index 00000000..8c3ed26d --- /dev/null +++ b/src/typings/refs.d.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ +/// \ No newline at end of file diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 00000000..79bf593f --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Promise = monaco.Promise; + +import * as cssService from 'vscode-css-languageservice/lib/cssLanguageService'; +import * as ls from 'vscode-languageserver-types/lib/main'; + +export class CSSWorker { + + // --- model sync ----------------------- + + private _languageService : cssService.LanguageService; + private _languageSettings: cssService.LanguageSettings; + private _languageId: string; + + constructor(createData:ICreateData) { + this._languageSettings = createData.languageSettings; + this._languageId = createData.languageId; + switch (this._languageId) { + case 'css': + this._languageService = cssService.getCSSLanguageService(); + break; + case 'less': + this._languageService = cssService.getLESSLanguageService(); + break; + case 'scss': + this._languageService = cssService.getSCSSLanguageService(); + break; + default: + throw new Error('Invalid language id: ' + this._languageId); + } + this._languageService.configure(this._languageSettings); + } + + // --- language service host --------------- + + doValidation(uri: string): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let diagnostics = this._languageService.doValidation(document, stylesheet); + return Promise.as(diagnostics) + } + doComplete(uri: string, position: ls.Position): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let completions = this._languageService.doComplete(document, position, stylesheet); + return Promise.as(completions); + } + doHover(uri: string, position: ls.Position): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let hover = this._languageService.doHover(document, position, stylesheet); + return Promise.as(hover); + } + findDefinition(uri: string, position: ls.Position): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let definition = this._languageService.findDefinition(document, position, stylesheet); + return Promise.as(definition); + } + findReferences(uri: string, position: ls.Position): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let references = this._languageService.findReferences(document, position, stylesheet); + return Promise.as(references); + } + findDocumentHighlights(uri: string, position: ls.Position): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let highlights = this._languageService.findDocumentHighlights(document, position, stylesheet); + return Promise.as(highlights); + } + findDocumentSymbols(uri: string): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let symbols = this._languageService.findDocumentSymbols(document, stylesheet); + return Promise.as(symbols); + } + doCodeActions(uri: string, range: ls.Range, context: ls.CodeActionContext): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let actions = this._languageService.doCodeActions(document, range, context, stylesheet); + return Promise.as(actions); + } + findColorSymbols(uri: string): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let colorSymbols = this._languageService.findColorSymbols(document, stylesheet); + return Promise.as(colorSymbols); + } + doRename(uri: string, position: ls.Position, newName: string): Promise { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseStylesheet(document); + let renames = this._languageService.doRename(document, position, newName, stylesheet); + return Promise.as(renames); + } + private _getTextDocument(uri:string): ls.TextDocument { + let models = monaco.worker.getMirrorModels(); + for (let model of models) { + if (model.uri.toString() === uri) { + return ls.TextDocument.create(uri, this._languageId, model.version, model.getText()); + } + } + return null; + } +} + +export interface ICreateData { + languageId: string; + languageSettings: cssService.LanguageSettings; +} + +export function create(createData:ICreateData): CSSWorker { + return new CSSWorker(createData); +} diff --git a/src/workerManager.ts b/src/workerManager.ts new file mode 100644 index 00000000..4c4400d4 --- /dev/null +++ b/src/workerManager.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {CSSWorker} from './worker'; + +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; + private _client: Promise; + + 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 { + this._lastUsedTime = Date.now(); + + if (!this._client) { + this._worker = monaco.editor.createWebWorker({ + + // module that exports the create() method and returns a `CSSWorker` instance + moduleId: 'vs/language/css/src/worker', + + // passed in to the create() method + createData: { + languageSettings: this._defaults.diagnosticsOptions + } + }); + + this._client = this._worker.getProxy(); + } + + return this._client; + } + + getLanguageServiceWorker(...resources: Uri[]): Promise { + let _client:CSSWorker; + return toShallowCancelPromise( + this._getClient().then((client) => { + _client = client + }).then(_ => { + return this._worker.withSyncedResources(resources) + }).then(_ => _client) + ); + } +} + +function toShallowCancelPromise(p:Promise): Promise { + let completeCallback: (value:T)=>void; + let errorCallback: (err:any)=>void; + + let r = new Promise((c, e) => { + completeCallback = c; + errorCallback = e; + }, () => { }); + + p.then(completeCallback, errorCallback); + + return r; +}