From a3416c2cfade3085a9af3a7f3f4508bbebaecf13 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 20 Sep 2018 18:49:12 +0300 Subject: [PATCH] [plug-in] Languages API - Register formatting provider for a document range Signed-off-by: Oleksii Kurinnyi --- packages/plugin-ext/src/api/plugin-api.ts | 2 + .../src/main/browser/languages-main.ts | 25 +++++++++- packages/plugin-ext/src/plugin/languages.ts | 16 ++++++- .../src/plugin/languages/range-formatting.ts | 47 +++++++++++++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 3 ++ .../plugin-ext/src/plugin/type-converters.ts | 2 +- packages/plugin/src/theia.d.ts | 45 ++++++++++++++++++ 7 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-ext/src/plugin/languages/range-formatting.ts diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index d60f2f7c62110..ddb3662334401 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -648,6 +648,7 @@ export interface LanguagesExt { $provideSignatureHelp(handle: number, resource: UriComponents, position: Position): Promise; $provideHover(handle: number, resource: UriComponents, position: Position): Promise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: FormattingOptions): Promise; + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: Range, options: FormattingOptions): Promise; } export interface LanguagesMain { @@ -662,6 +663,7 @@ export interface LanguagesMain { $clearDiagnostics(id: string): void; $changeDiagnostics(id: string, delta: [UriComponents, MarkerData[]][]): void; $registerDocumentFormattingSupport(handle: number, selector: SerializedDocumentFilter[]): void; + $registerRangeFormattingProvider(handle: number, selector: SerializedDocumentFilter[]): void; } export const PLUGIN_RPC_CONTEXT = { diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index d94d2a8036554..e1dfa9212cba7 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -22,7 +22,7 @@ import { MAIN_RPC_CONTEXT, LanguagesExt } from '../../api/plugin-api'; -import { SerializedDocumentFilter, MarkerData } from '../../api/model'; +import { SerializedDocumentFilter, MarkerData, Range } from '../../api/model'; import { RPCProtocol } from '../../api/rpc-protocol'; import { fromLanguageSelector } from '../../plugin/type-converters'; import { UriComponents } from '@theia/plugin-ext/src/common/uri-components'; @@ -219,6 +219,29 @@ export class LanguagesMainImpl implements LanguagesMain { }; } + $registerRangeFormattingProvider(handle: number, selector: SerializedDocumentFilter[]): void { + const languageSelector = fromLanguageSelector(selector); + const rangeFormattingEditProvider = this.createRangeFormattingProvider(handle, languageSelector); + const disposable = new DisposableCollection(); + for (const language of getLanguages()) { + if (this.matchLanguage(languageSelector, language)) { + disposable.push(monaco.languages.registerDocumentRangeFormattingEditProvider(language, rangeFormattingEditProvider)); + } + } + this.disposables.set(handle, disposable); + } + + createRangeFormattingProvider(handle: number, selector: LanguageSelector | undefined): monaco.languages.DocumentRangeFormattingEditProvider { + return { + provideDocumentRangeFormattingEdits: (model, range: Range, options, token) => { + if (!this.matchModel(selector, MonacoModelIdentifier.fromModel(model))) { + return undefined!; + } + return this.proxy.$provideDocumentRangeFormattingEdits(handle, model.uri, range, options).then(v => v!); + } + }; + } + protected matchModel(selector: LanguageSelector | undefined, model: MonacoModelIdentifier): boolean { if (Array.isArray(selector)) { return selector.some(filter => this.matchModel(filter, model)); diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 71a681c23ea2c..b48571cd4e273 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -38,6 +38,7 @@ import { SerializedDocumentFilter, SignatureHelp, Hover, + Range, SingleEditOperation, FormattingOptions, Definition, @@ -48,9 +49,10 @@ import { Diagnostics } from './languages/diagnostics'; import { SignatureHelpAdapter } from './languages/signature'; import { HoverAdapter } from './languages/hover'; import { DocumentFormattingAdapter } from './languages/document-formatting'; +import { RangeFormattingAdapter } from './languages/range-formatting'; import { DefinitionAdapter } from './languages/definition'; -type Adapter = CompletionAdapter | SignatureHelpAdapter | HoverAdapter | DocumentFormattingAdapter | DefinitionAdapter; +type Adapter = CompletionAdapter | SignatureHelpAdapter | HoverAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | DefinitionAdapter; export class LanguagesExtImpl implements LanguagesExt { @@ -229,6 +231,18 @@ export class LanguagesExtImpl implements LanguagesExt { } // ### Document Formatting Edit end + // ### Document Range Formatting Edit begin + registerDocumentRangeFormattingEditProvider(selector: theia.DocumentSelector, provider: theia.DocumentRangeFormattingEditProvider): theia.Disposable { + const callId = this.addNewAdapter(new RangeFormattingAdapter(provider, this.documents)); + this.proxy.$registerRangeFormattingProvider(callId, this.transformDocumentSelector(selector)); + return this.createDisposable(callId); + } + + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: Range, options: FormattingOptions): Promise { + return this.withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options)); + } + // ### Document Range Formatting Edit end + } function serializeEnterRules(rules?: theia.OnEnterRule[]): SerializedOnEnterRule[] | undefined { diff --git a/packages/plugin-ext/src/plugin/languages/range-formatting.ts b/packages/plugin-ext/src/plugin/languages/range-formatting.ts new file mode 100644 index 0000000000000..07cfd17262477 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/range-formatting.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as theia from '@theia/plugin'; +import { DocumentsExtImpl } from '../documents'; +import * as Converter from '../type-converters'; +import URI from 'vscode-uri/lib/umd'; +import { FormattingOptions, SingleEditOperation, Range } from '../../api/model'; + +export class RangeFormattingAdapter { + + constructor( + private readonly provider: theia.DocumentRangeFormattingEditProvider, + private readonly documents: DocumentsExtImpl + ) { } + + provideDocumentRangeFormattingEdits(resource: URI, range: Range, options: FormattingOptions): Promise { + const document = this.documents.getDocumentData(resource); + if (!document) { + return Promise.reject(new Error(`There are no document for ${resource}`)); + } + + const doc = document.document; + const ran = Converter.toRange(range); + + return Promise.resolve(this.provider.provideDocumentRangeFormattingEdits(doc, ran, options, undefined)).then(value => { + if (Array.isArray(value)) { + return value.map(Converter.fromTextEdit); + } + return undefined; + }); + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 25c3de23a66d7..3f240caf4aad3 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -309,6 +309,9 @@ export function createAPIFactory(rpc: RPCProtocol, pluginManager: PluginManager) }, registerDocumentFormattingEditProvider(selector: theia.DocumentSelector, provider: theia.DocumentFormattingEditProvider): theia.Disposable { return languagesExt.registerDocumentFormattingEditProvider(selector, provider); + }, + registerDocumentRangeFormattingEditProvider(selector: theia.DocumentSelector, provider: theia.DocumentRangeFormattingEditProvider): theia.Disposable { + return languagesExt.registerDocumentRangeFormattingEditProvider(selector, provider); } }; diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 3515eef1abb06..3cdfcbada779d 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -286,7 +286,7 @@ export function toCompletionItemKind(type?: CompletionType): types.CompletionIte export function fromTextEdit(edit: theia.TextEdit): SingleEditOperation { return { text: edit.newText, - range: fromRange(edit.range) + range: fromRange_(edit.range) }; } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 025257b1c499f..bb0867010a0b9 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3649,6 +3649,34 @@ declare module '@theia/plugin' { ): ProviderResult; } + /** + * The document formatting provider interface defines the contract between extensions and + * the formatting-feature. + */ + export interface DocumentRangeFormattingEditProvider { + + /** + * Provide formatting edits for a range in a document. + * + * The given range is a hint and providers can decide to format a smaller + * or larger range. Often this is done by adjusting the start and end + * of the range to full syntax nodes. + * + * @param document The document in which the command was invoked. + * @param range The range which should be formatted. + * @param options Options controlling formatting. + * @param token A cancellation token. + * @return A set of text edits or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty array. + */ + provideDocumentRangeFormattingEdits( + document: TextDocument, + range: Range, + options: FormattingOptions, + token: CancellationToken | undefined + ): ProviderResult; + } + /** * Value-object describing what options formatting should use. */ @@ -3824,6 +3852,23 @@ declare module '@theia/plugin' { * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerDocumentFormattingEditProvider(selector: DocumentSelector, provider: DocumentFormattingEditProvider): Disposable; + + /** + * Register a formatting provider for a document range. + * + * *Note:* A document range provider is also a [document formatter](#DocumentFormattingEditProvider) + * which means there is no need to [register](registerDocumentFormattingEditProvider) a document + * formatter when also registering a range provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A document range formatting edit provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerDocumentRangeFormattingEditProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider): Disposable; } /**