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..c16d57c5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +/.vscode/ +/lib/ +/out/ +/src/ +/test/ +/release/dev/ +/gulpfile.js +/.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 ea3b41b1..66a16e0f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# monaco-json -JSON language support for the Monaco editor +# Monaco JSON + +JSON language plugin for the Monaco Editor. + + +## 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-json/blob/master/LICENSE.md) diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..dae01120 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * 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-json version: ' + headerVersion, + ' * Released under the MIT license', + ' * https://github.com/Microsoft/monaco-json/blob/master/LICENSE.md', + ' *-----------------------------------------------------------------------------*/', + '' + ].join('\n'); + + function bundleOne(moduleId, exclude) { + return rjs({ + baseUrl: '/out/', + name: 'vs/language/json/' + moduleId, + out: moduleId + '.js', + exclude: exclude, + paths: { + 'vs/language/json': __dirname + '/out' + }, + packages: [{ + name: 'vscode-json-languageservice', + location: __dirname + '/node_modules/vscode-json-languageservice/lib', + main: 'jsonLanguageService' + }, { + name: 'vscode-languageserver-types', + location: __dirname + '/node_modules/vscode-languageserver-types/lib', + main: 'main' + }, { + name: 'jsonc-parser', + location: __dirname + '/node_modules/jsonc-parser/lib', + main: 'main' + }, { + name: 'vscode-nls', + location: __dirname + '/out/fillers', + main: 'vscode-nls' + }] + }) + } + + return merge( + merge( + bundleOne('monaco.contribution', ['vs/language/json/jsonMode']), + bundleOne('jsonMode'), + bundleOne('jsonWorker') + ) + .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]; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..7ef4e909 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "monaco-json", + "version": "1.0.0", + "description": "JSON plugin for the Monaco Editor", + "scripts": { + "compile": "gulp compile", + "watch": "gulp watch", + "prepublish": "gulp release" + }, + "author": "Microsoft Corporation", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/monaco-json" + }, + "bugs": { + "url": "https://github.com/Microsoft/monaco-json/issues" + }, + "dependencies": { + "vscode-json-languageservice": "^1.0.0", + "vscode-languageserver-types": "^1.0.1" + }, + "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.5.1", + "object-assign": "^4.1.0", + "rimraf": "^2.5.2", + "typescript": "1.8.10" + } +} diff --git a/src/fillers/vscode-nls.ts b/src/fillers/vscode-nls.ts new file mode 100644 index 00000000..b9566a77 --- /dev/null +++ b/src/fillers/vscode-nls.ts @@ -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; +} \ No newline at end of file diff --git a/src/jsonMode.ts b/src/jsonMode.ts new file mode 100644 index 00000000..a3047456 --- /dev/null +++ b/src/jsonMode.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {JSONWorker} from './jsonWorker'; +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 => { + return client.getLanguageServiceWorker(...uris); + }; + + let languageId = defaults.languageId; + + disposables.push(monaco.languages.registerCompletionItemProvider(languageId, new languageFeatures.CompletionAdapter(worker))); + disposables.push(monaco.languages.registerHoverProvider(languageId, new languageFeatures.HoverAdapter(worker))); + disposables.push(monaco.languages.registerDocumentSymbolProvider(languageId, new languageFeatures.DocumentSymbolAdapter(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.setTokensProvider(languageId, createTokenizationSupport(true))); + disposables.push(monaco.languages.setLanguageConfiguration(languageId, richEditConfiguration)); +} + + +const richEditConfiguration: monaco.languages.LanguageConfiguration = { + wordPattern: /(-?\d*\.\d\w*)|([^\[\{\]\}\:\"\,\s]+)/g, + + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + + brackets: [ + ['{', '}'], + ['[', ']'] + ], + + autoClosingPairs: [ + { open: '{', close: '}', notIn: ['string'] }, + { open: '[', close: ']', notIn: ['string'] }, + { open: '"', close: '"', notIn: ['string'] } + ] +}; + diff --git a/src/jsonWorker.ts b/src/jsonWorker.ts new file mode 100644 index 00000000..f826609b --- /dev/null +++ b/src/jsonWorker.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Thenable = monaco.Thenable; +import IWorkerContext = monaco.worker.IWorkerContext; + +import * as jsonService from 'vscode-json-languageservice'; +import * as ls from 'vscode-languageserver-types'; + + +class PromiseAdapter implements jsonService.Thenable { + private wrapped: monaco.Promise; + + constructor(executor: (resolve: (value?: T | jsonService.Thenable) => void, reject: (reason?: any) => void) => void) { + this.wrapped = new monaco.Promise(executor); + } + public then(onfulfilled?: (value: T) => TResult | jsonService.Thenable, onrejected?: (reason: any) => void): jsonService.Thenable { + return this.wrapped.then(onfulfilled, onrejected); + } + public getWrapped(): monaco.Promise { + return this.wrapped; + } + public cancel() : void { + this.wrapped.cancel(); + } + + public static resolve(v: T) : jsonService.Thenable { + return monaco.Promise.as(v); + } + public static reject(v: T) : jsonService.Thenable { + return monaco.Promise.wrapError(v); + } + public static all(values: jsonService.Thenable[]): jsonService.Thenable { + return monaco.Promise.join(values); + } +} + +function toMonacoPromise(thenable: jsonService.Thenable) : Thenable { + if (thenable instanceof PromiseAdapter) { + return thenable.getWrapped(); + } + return thenable; +} + +export class JSONWorker { + + private _ctx:IWorkerContext; + private _languageService: jsonService.LanguageService; + private _languageSettings: jsonService.LanguageSettings; + private _languageId: string; + + constructor(ctx:IWorkerContext, createData: ICreateData) { + this._ctx = ctx; + this._languageSettings = createData.languageSettings; + this._languageId = createData.languageId; + this._languageService = jsonService.getLanguageService({ promiseConstructor: PromiseAdapter }); + this._languageService.configure(this._languageSettings); + } + + doValidation(uri: string): Thenable { + let document = this._getTextDocument(uri); + let jsonDocument = this._languageService.parseJSONDocument(document); + return this._languageService.doValidation(document, jsonDocument); + } + doComplete(uri: string, position: ls.Position): Thenable { + let document = this._getTextDocument(uri); + let jsonDocument = this._languageService.parseJSONDocument(document); + return this._languageService.doComplete(document, position, jsonDocument); + } + doResolve(item: ls.CompletionItem): Thenable { + return this._languageService.doResolve(item); + } + doHover(uri: string, position: ls.Position): Thenable { + let document = this._getTextDocument(uri); + let jsonDocument = this._languageService.parseJSONDocument(document); + return this._languageService.doHover(document, position, jsonDocument); + } + format(uri: string, range: ls.Range, options: ls.FormattingOptions): Thenable { + let document = this._getTextDocument(uri); + let textEdits = this._languageService.format(document, range, options); + return Promise.as(textEdits); + } + resetSchema(uri: string): Thenable { + return Promise.as(this._languageService.resetSchema(uri)); + } + findDocumentSymbols(uri: string): Promise { + let document = this._getTextDocument(uri); + let jsonDocument = this._languageService.parseJSONDocument(document); + let symbols = this._languageService.findDocumentSymbols(document, jsonDocument); + return Promise.as(symbols); + } + 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: jsonService.LanguageSettings; +} + +export function create(ctx:IWorkerContext, createData: ICreateData): JSONWorker { + return new JSONWorker(ctx, createData); +} diff --git a/src/languageFeatures.ts b/src/languageFeatures.ts new file mode 100644 index 00000000..2813aa72 --- /dev/null +++ b/src/languageFeatures.ts @@ -0,0 +1,451 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {JSONWorker} from './jsonWorker'; + +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 +} + +// --- 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 => { + delete this._listener[model.uri.toString()]; + }; + + this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd)); + this._disposables.push(monaco.editor.onWillDisposeModel(model => { + onModelRemoved(model); + this._resetSchema(model.uri); + })); + this._disposables.push(monaco.editor.onDidChangeModelLanguage(event => { + onModelRemoved(event.model); + onModelAdd(event.model); + this._resetSchema(event.model.uri); + })); + + 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 _resetSchema(resource: Uri): void { + this._worker().then(worker => { + worker.resetSchema(resource.toString()); + }); + } + + 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) : 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 { + 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 { + isIncomplete: info.isIncomplete, + items: items + }; + }); + })); + } + + resolveCompletionItem(item: monaco.languages.CompletionItem, token: CancellationToken): Thenable { + return wireCancellationToken(token, this._worker().then(worker => { + let lsItem = fromCompletionItem(item); + return worker.doResolve(lsItem).then(result => { + return toCompletionItem(result); + }); + })); + } +} + +function toMarkedStringArray(contents: ls.MarkedString | ls.MarkedString[]): monaco.MarkedString[] { + if (!contents) { + return void 0; + } + if (Array.isArray(contents)) { + return (contents); + } + return [contents]; +} + + +// --- hover ------ + +export class HoverAdapter implements monaco.languages.HoverProvider { + + constructor(private _worker: WorkerAccessor) { + } + + 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), + contents: toMarkedStringArray(info.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; +} + + +export class DocumentSymbolAdapter implements monaco.languages.DocumentSymbolProvider { + + constructor(private _worker: WorkerAccessor) { + } + + 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) + })); + })); + } +} + + +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 { + 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 { + 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(token: CancellationToken, promise: Thenable): Thenable { + if ((>promise).cancel) { + token.onCancellationRequested(() => (>promise).cancel()); + } + return promise; +} diff --git a/src/monaco.contribution.ts b/src/monaco.contribution.ts new file mode 100644 index 00000000..47943560 --- /dev/null +++ b/src/monaco.contribution.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './jsonMode'; + +import Emitter = monaco.Emitter; +import IEvent = monaco.IEvent; +import IDisposable = monaco.IDisposable; + +declare var require: (moduleId: [string], callback: (module: T) => void) => void; + +// --- JSON configuration and defaults --------- + +export class LanguageServiceDefaultsImpl implements monaco.languages.json.LanguageServiceDefaults { + + private _onDidChange = new Emitter(); + private _diagnosticsOptions: monaco.languages.json.DiagnosticsOptions; + private _languageId: string; + + constructor(languageId: string, diagnosticsOptions: monaco.languages.json.DiagnosticsOptions) { + this._languageId = languageId; + this.setDiagnosticsOptions(diagnosticsOptions); + } + + get onDidChange(): IEvent { + return this._onDidChange.event; + } + + get languageId(): string { + return this._languageId; + } + + get diagnosticsOptions(): monaco.languages.json.DiagnosticsOptions { + return this._diagnosticsOptions; + } + + setDiagnosticsOptions(options: monaco.languages.json.DiagnosticsOptions): void { + this._diagnosticsOptions = options || Object.create(null); + this._onDidChange.fire(this); + } +} + +const diagnosticDefault: monaco.languages.json.DiagnosticsOptions = { + validate: true, + allowComments: true, + schemas: [] +} + +const jsonDefaults = new LanguageServiceDefaultsImpl('json', diagnosticDefault); + + +// Export API +function createAPI(): typeof monaco.languages.json { + return { + jsonDefaults: jsonDefaults, + } +} +monaco.languages.json = createAPI(); + +// --- Registration to monaco editor --- + +function withMode(callback: (module: typeof mode) => void): void { + require(['vs/language/json/jsonMode'], callback); +} + +monaco.languages.register({ + id: 'json', + extensions: ['.json', '.bowerrc', '.jshintrc', '.jscsrc', '.eslintrc', '.babelrc'], + aliases: ['JSON', 'json'], + mimetypes: ['application/json'], +}); +monaco.languages.onLanguage('json', () => { + withMode(mode => mode.setupMode(jsonDefaults)); +}); diff --git a/src/monaco.d.ts b/src/monaco.d.ts new file mode 100644 index 00000000..1d3b1c99 --- /dev/null +++ b/src/monaco.d.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * 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.json { + export interface DiagnosticsOptions { + /** + * If set, the validator will return syntax errors. + */ + validate?: boolean; + /** + * If set, comments are toleranted. If not set, a syntax error is emmited for comments. + */ + allowComments?: boolean; + /** + * A list of known schemas and/or associations of schemas to file names. + */ + schemas?: { + /** + * The URI of the schema, which is also the identifier of the schema. + */ + uri: string; + /** + * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' + */ + fileMatch?: string[]; + /** + * The schema for the given URI. + */ + schema?: any; + }[]; + } + + export interface LanguageServiceDefaults { + onDidChange: IEvent; + diagnosticsOptions: DiagnosticsOptions; + setDiagnosticsOptions(options: DiagnosticsOptions): void; + } + + export var jsonDefaults: LanguageServiceDefaults; +} \ No newline at end of file diff --git a/src/tokenization.ts b/src/tokenization.ts new file mode 100644 index 00000000..28875efa --- /dev/null +++ b/src/tokenization.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * 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 json = require('jsonc-parser'); + +export function createTokenizationSupport(supportComments: boolean): monaco.languages.TokensProvider { + return { + getInitialState: () => new JSONState(null, null, false), + tokenize: (line, state, offsetDelta?, stopAtOffset?) => tokenize(supportComments, line, state, offsetDelta, stopAtOffset) + }; +} + +export const TOKEN_DELIM_OBJECT = 'punctuation.bracket.json'; +export const TOKEN_DELIM_ARRAY = 'punctuation.array.json'; +export const TOKEN_DELIM_COLON = 'punctuation.colon.json'; +export const TOKEN_DELIM_COMMA = 'punctuation.comma.json'; +export const TOKEN_VALUE_BOOLEAN = 'support.property-value.keyword.json'; +export const TOKEN_VALUE_NULL = 'support.property-value.constant.other.json'; +export const TOKEN_VALUE_STRING = 'support.property-value.string.value.json'; +export const TOKEN_VALUE_NUMBER = 'support.property-value.constant.numeric.json'; +export const TOKEN_PROPERTY_NAME = 'support.type.property-name.json'; +export const TOKEN_COMMENT_BLOCK = 'comment.block.json'; +export const TOKEN_COMMENT_LINE = 'comment.line.json'; + +class JSONState implements monaco.languages.IState { + + private _state: monaco.languages.IState; + + public scanError: json.ScanError; + public lastWasColon: boolean; + + constructor(state: monaco.languages.IState, scanError: json.ScanError, lastWasColon: boolean) { + this._state = state; + this.scanError = scanError; + this.lastWasColon = lastWasColon; + } + + public clone(): JSONState { + return new JSONState(this._state, this.scanError, this.lastWasColon); + } + + public equals(other: monaco.languages.IState): boolean { + if (other === this) { + return true; + } + if (!other || !(other instanceof JSONState)) { + return false; + } + return this.scanError === (other).scanError && + this.lastWasColon === (other).lastWasColon; + } + + public getStateData(): monaco.languages.IState { + return this._state; + } + + public setStateData(state: monaco.languages.IState): void { + this._state = state; + } +} + +function tokenize(comments: boolean, line: string, state: JSONState, offsetDelta: number = 0, stopAtOffset?: number): monaco.languages.ILineTokens { + + // handle multiline strings and block comments + var numberOfInsertedCharacters = 0, + adjustOffset = false; + + switch (state.scanError) { + case json.ScanError.UnexpectedEndOfString: + line = '"' + line; + numberOfInsertedCharacters = 1; + break; + case json.ScanError.UnexpectedEndOfComment: + line = '/*' + line; + numberOfInsertedCharacters = 2; + break; + } + + var scanner = json.createScanner(line), + kind: json.SyntaxKind, + ret: monaco.languages.ILineTokens, + lastWasColon = state.lastWasColon; + + ret = { + tokens: [], + endState: state.clone() + }; + + while (true) { + + var offset = offsetDelta + scanner.getPosition(), + type = ''; + + kind = scanner.scan(); + if (kind === json.SyntaxKind.EOF) { + break; + } + + // Check that the scanner has advanced + if (offset === offsetDelta + scanner.getPosition()) { + throw new Error('Scanner did not advance, next 3 characters are: ' + line.substr(scanner.getPosition(), 3)); + } + + // In case we inserted /* or " character, we need to + // adjust the offset of all tokens (except the first) + if (adjustOffset) { + offset -= numberOfInsertedCharacters; + } + adjustOffset = numberOfInsertedCharacters > 0; + + + // brackets and type + switch (kind) { + case json.SyntaxKind.OpenBraceToken: + type = TOKEN_DELIM_OBJECT; + lastWasColon = false; + break; + case json.SyntaxKind.CloseBraceToken: + type = TOKEN_DELIM_OBJECT; + lastWasColon = false; + break; + case json.SyntaxKind.OpenBracketToken: + type = TOKEN_DELIM_ARRAY; + lastWasColon = false; + break; + case json.SyntaxKind.CloseBracketToken: + type = TOKEN_DELIM_ARRAY; + lastWasColon = false; + break; + case json.SyntaxKind.ColonToken: + type = TOKEN_DELIM_COLON; + lastWasColon = true; + break; + case json.SyntaxKind.CommaToken: + type = TOKEN_DELIM_COMMA; + lastWasColon = false; + break; + case json.SyntaxKind.TrueKeyword: + case json.SyntaxKind.FalseKeyword: + type = TOKEN_VALUE_BOOLEAN; + lastWasColon = false; + break; + case json.SyntaxKind.NullKeyword: + type = TOKEN_VALUE_NULL; + lastWasColon = false; + break; + case json.SyntaxKind.StringLiteral: + type = lastWasColon ? TOKEN_VALUE_STRING : TOKEN_PROPERTY_NAME; + lastWasColon = false; + break; + case json.SyntaxKind.NumericLiteral: + type = TOKEN_VALUE_NUMBER; + lastWasColon = false; + break; + } + + // comments, iff enabled + if (comments) { + switch (kind) { + case json.SyntaxKind.LineCommentTrivia: + type = TOKEN_COMMENT_LINE; + break; + case json.SyntaxKind.BlockCommentTrivia: + type = TOKEN_COMMENT_BLOCK; + break; + } + } + + ret.endState = new JSONState(state.getStateData(), scanner.getTokenError(), lastWasColon); + ret.tokens.push({ + startIndex: offset, + scopes: type + }); + } + + return ret; +} \ 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/workerManager.ts b/src/workerManager.ts new file mode 100644 index 00000000..f75a473e --- /dev/null +++ b/src/workerManager.ts @@ -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 {JSONWorker} from './jsonWorker'; + +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 `JSONWorker` instance + moduleId: 'vs/language/json/jsonWorker', + + // passed in to the create() method + createData: { + languageSettings: this._defaults.diagnosticsOptions, + languageId: this._defaults.languageId + } + }); + + this._client = this._worker.getProxy(); + } + + return this._client; + } + + getLanguageServiceWorker(...resources: Uri[]): Promise { + let _client: JSONWorker; + 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; +}