From d664c3ecfb2f33118e651ad084b8c59edbc384be Mon Sep 17 00:00:00 2001 From: Vitaliy Gulyy Date: Tue, 15 Sep 2020 13:00:04 +0300 Subject: [PATCH] Add ability to set default formatter Signed-off-by: Vitaliy Gulyy --- .../editor/src/browser/editor-preferences.ts | 9 +- .../browser/monaco-formatting-conflicts.ts | 149 ++++++++++++++++++ .../src/browser/monaco-frontend-module.ts | 6 + packages/monaco/src/browser/monaco-loader.ts | 3 + packages/monaco/src/typings/monaco/index.d.ts | 41 +++++ .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../src/main/browser/languages-main.ts | 32 ++-- packages/plugin-ext/src/plugin/languages.ts | 2 +- .../browser/plugin-metrics-languages-main.ts | 4 +- 9 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 packages/monaco/src/browser/monaco-formatting-conflicts.ts diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index 552ebc729e4aa..0a0652e0fab18 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -58,9 +58,9 @@ export const EDITOR_MODEL_DEFAULTS = { largeFileOptimizations: true }; -/* eslint-enable no-null/no-null */ - /* eslint-disable max-len */ +/* eslint-disable no-null/no-null */ + // should be in sync with: // 1. https://github.com/theia-ide/vscode/blob/standalone/0.20.x/src/vs/editor/common/config/commonEditorConfig.ts#L441 // 2. https://github.com/theia-ide/vscode/blob/standalone/0.20.x/src/vs/editor/common/config/commonEditorConfig.ts#L530 @@ -77,6 +77,11 @@ const codeEditorPreferenceProperties = { 'minimum': 1, 'markdownDescription': 'The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.' }, + 'editor.defaultFormatter': { + 'type': 'string', + 'default': null, + 'description': 'Default formatter' + }, 'editor.insertSpaces': { 'type': 'boolean', 'default': EDITOR_MODEL_DEFAULTS.insertSpaces, diff --git a/packages/monaco/src/browser/monaco-formatting-conflicts.ts b/packages/monaco/src/browser/monaco-formatting-conflicts.ts new file mode 100644 index 0000000000000..63268e4b3efbe --- /dev/null +++ b/packages/monaco/src/browser/monaco-formatting-conflicts.ts @@ -0,0 +1,149 @@ +/******************************************************************************** + * Copyright (C) 2020 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 { injectable, inject } from 'inversify'; +import { MonacoQuickOpenService } from './monaco-quick-open-service'; +import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from '@theia/core/lib/common/quick-open-model'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { PreferenceService, FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { PreferenceSchemaProvider } from '@theia/core/lib/browser'; +import { EditorManager } from '@theia/editor/lib/browser'; + +type FormattingEditProvider = monaco.languages.DocumentFormattingEditProvider | monaco.languages.DocumentRangeFormattingEditProvider; + +const PREFERENCE_NAME = 'editor.defaultFormatter'; + +@injectable() +export class MonacoFormattingConflictsContribution implements FrontendApplicationContribution { + + @inject(MonacoQuickOpenService) + protected readonly quickOpenService: MonacoQuickOpenService; + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + + @inject(PreferenceSchemaProvider) + protected readonly preferenceSchema: PreferenceSchemaProvider; + + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + async initialize(): Promise { + monaco.format.FormattingConflicts.setFormatterSelector(( + formatters: T[], document: monaco.editor.ITextModel, mode: monaco.format.FormattingMode) => + this.selectFormatter(formatters, document, mode)); + } + + private async setDefaultFormatter(language: string, formatter: string): Promise { + const name = this.preferenceSchema.overridePreferenceName({ + preferenceName: PREFERENCE_NAME, + overrideIdentifier: language + }); + + await this.preferenceService.set(name, formatter); + } + + private getDefaultFormatter(language: string): string | undefined { + const name = this.preferenceSchema.overridePreferenceName({ + preferenceName: PREFERENCE_NAME, + overrideIdentifier: language + }); + + return this.preferenceService.get(name); + } + + private async selectFormatter( + formatters: T[], document: monaco.editor.ITextModel, mode: monaco.format.FormattingMode): Promise { + + if (formatters.length === 0) { + return undefined; + } + + if (formatters.length === 1) { + return formatters[0]; + } + + const currentEditor = this.editorManager.currentEditor; + if (!currentEditor) { + return undefined; + } + + const languageId = currentEditor.editor.document.languageId; + const defaultFormatterId = await this.getDefaultFormatter(languageId); + + if (defaultFormatterId) { + const formatter = formatters.find(f => f.extensionId && f.extensionId.value === defaultFormatterId); + if (formatter) { + return formatter; + } + } + + let deferred: Deferred | undefined = new Deferred(); + + const items: QuickOpenItem[] = formatters + .filter(formatter => formatter.displayName) + .map(formatter => { + const displayName: string = formatter.displayName!; + const extensionId = formatter.extensionId ? formatter.extensionId.value : undefined; + + return new QuickOpenItem({ + label: displayName, + detail: extensionId, + run: (openMode: QuickOpenMode) => { + if (openMode === QuickOpenMode.OPEN) { + if (deferred) { + deferred.resolve(formatter); + deferred = undefined; + } + + this.quickOpenService.hide(); + + this.setDefaultFormatter(languageId, extensionId ? extensionId : ''); + return true; + } + + return false; + } + }); + }) + .sort((a, b) => a.getLabel()!.localeCompare(b.getLabel()!)); + + const model: QuickOpenModel = { + onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { + acceptor(items); + } + }; + + this.quickOpenService.open(model, + { + fuzzyMatchDescription: true, + fuzzyMatchLabel: true, + fuzzyMatchDetail: true, + placeholder: 'Select formatter for the current document', + ignoreFocusOut: false, + + onClose: () => { + if (deferred) { + deferred.resolve(undefined); + deferred = undefined; + } + } + }); + + return deferred.promise; + } + +} diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index acb05d9cda84d..f73c1f94c0501 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -61,6 +61,7 @@ import { WorkspaceSymbolCommand } from './workspace-symbol-command'; import { LanguageService } from '@theia/core/lib/browser/language-service'; import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter'; +import { MonacoFormattingConflictsContribution } from './monaco-formatting-conflicts'; decorate(injectable(), monaco.contextKeyService.ContextKeyService); @@ -103,9 +104,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindContributionProvider(bind, MonacoEditorModelFactory); bind(MonacoCommandService).toSelf().inTransientScope(); bind(MonacoCommandServiceFactory).toAutoFactory(MonacoCommandService); + bind(TextEditorProvider).toProvider(context => uri => context.container.get(MonacoEditorProvider).get(uri) ); + bind(MonacoDiffNavigatorFactory).toSelf().inSingletonScope(); bind(DiffNavigatorProvider).toFactory(context => editor => context.container.get(MonacoEditorProvider).getDiffNavigator(editor) @@ -114,6 +117,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoOutlineContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(MonacoOutlineContribution); + bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(MonacoFormattingConflictsContribution); + bind(MonacoStatusBarContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(MonacoStatusBarContribution); diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts index a077f8d280850..e6d974da208c1 100644 --- a/packages/monaco/src/browser/monaco-loader.ts +++ b/packages/monaco/src/browser/monaco-loader.ts @@ -67,6 +67,7 @@ export function loadMonaco(vsRequire: any): Promise { 'vs/editor/common/modes', 'vs/editor/contrib/suggest/suggest', 'vs/editor/contrib/snippet/snippetParser', + 'vs/editor/contrib/format/format', 'vs/platform/configuration/common/configuration', 'vs/platform/configuration/common/configurationModels', 'vs/editor/common/services/resolverService', @@ -86,6 +87,7 @@ export function loadMonaco(vsRequire: any): Promise { standaloneServices: any, standaloneLanguages: any, quickOpenWidget: any, quickOpenModel: any, filters: any, themeService: any, styler: any, colorRegistry: any, color: any, platform: any, modes: any, suggest: any, snippetParser: any, + format: any, configuration: any, configurationModels: any, resolverService: any, codeEditorService: any, codeEditorServiceImpl: any, openerService: any, @@ -109,6 +111,7 @@ export function loadMonaco(vsRequire: any): Promise { global.monaco.modes = modes; global.monaco.suggest = suggest; global.monaco.snippetParser = snippetParser; + global.monaco.format = format; global.monaco.contextkey = contextKey; global.monaco.contextKeyService = contextKeyService; global.monaco.mime = mime; diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 63726c7e88c40..393917fc20856 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -17,6 +17,47 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /// +declare module monaco.languages { + + export class ExtensionIdentifier { + public readonly value: string; + } + + /** + * The document formatting provider interface defines the contract between extensions and + * the formatting-feature. + */ + export interface DocumentFormattingEditProvider { + readonly extensionId?: ExtensionIdentifier; + } + + /** + * The document formatting provider interface defines the contract between extensions and + * the formatting-feature. + */ + export interface DocumentRangeFormattingEditProvider { + readonly extensionId?: ExtensionIdentifier; + } + +} + +declare module monaco.format { + + export const enum FormattingMode { + Explicit = 1, + Silent = 2 + } + + export interface IFormattingEditProviderSelector { + (formatter: T[], document: monaco.editor.ITextModel, mode: FormattingMode): Promise; + } + + export abstract class FormattingConflicts { + static setFormatterSelector(selector: IFormattingEditProviderSelector): monaco.IDisposable; + } + +} + declare module monaco.instantiation { // https://github.com/theia-ide/vscode/blob/standalone/0.20.x/src/vs/platform/instantiation/common/instantiation.ts#L86 export interface IInstantiationService { diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index f1c29a3aba266..1a89b79249a55 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1307,7 +1307,7 @@ export interface LanguagesMain { $clearDiagnostics(id: string): void; $changeDiagnostics(id: string, delta: [string, MarkerData[]][]): void; $registerDocumentFormattingSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; - $registerRangeFormattingProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; + $registerRangeFormattingSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerOnTypeFormattingProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], autoFormatTriggerCharacters: string[]): void; $registerDocumentLinkProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerCodeLensSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], eventHandle?: number): void; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 1d502986b0da6..b5de631172c07 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -562,14 +562,21 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { $registerDocumentFormattingSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { const languageSelector = this.toLanguageSelector(selector); - const documentFormattingEditSupport = this.createDocumentFormattingSupport(handle); + const documentFormattingEditSupport = this.createDocumentFormattingSupport(handle, pluginInfo); this.register(handle, monaco.languages.registerDocumentFormattingEditProvider(languageSelector, documentFormattingEditSupport)); } - createDocumentFormattingSupport(handle: number): monaco.languages.DocumentFormattingEditProvider { - return { - provideDocumentFormattingEdits: (model, options, token) => this.provideDocumentFormattingEdits(handle, model, options, token) + createDocumentFormattingSupport(handle: number, pluginInfo: PluginInfo): monaco.languages.DocumentFormattingEditProvider { + const provider: monaco.languages.DocumentFormattingEditProvider = { + extensionId: { + value: pluginInfo.id + }, + displayName: pluginInfo.name, + provideDocumentFormattingEdits: (model, options, token) => + this.provideDocumentFormattingEdits(handle, model, options, token) }; + + return provider; } protected provideDocumentFormattingEdits(handle: number, model: monaco.editor.ITextModel, @@ -577,16 +584,23 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { return this.proxy.$provideDocumentFormattingEdits(handle, model.uri, options, token); } - $registerRangeFormattingProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { + $registerRangeFormattingSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { const languageSelector = this.toLanguageSelector(selector); - const rangeFormattingEditProvider = this.createRangeFormattingProvider(handle); + const rangeFormattingEditProvider = this.createRangeFormattingSupport(handle, pluginInfo); this.register(handle, monaco.languages.registerDocumentRangeFormattingEditProvider(languageSelector, rangeFormattingEditProvider)); } - createRangeFormattingProvider(handle: number): monaco.languages.DocumentRangeFormattingEditProvider { - return { - provideDocumentRangeFormattingEdits: (model, range: Range, options, token) => this.provideDocumentRangeFormattingEdits(handle, model, range, options, token) + createRangeFormattingSupport(handle: number, pluginInfo: PluginInfo): monaco.languages.DocumentRangeFormattingEditProvider { + const provider: monaco.languages.DocumentRangeFormattingEditProvider = { + extensionId: { + value: pluginInfo.id + }, + displayName: pluginInfo.name, + provideDocumentRangeFormattingEdits: (model, range: Range, options, token) => + this.provideDocumentRangeFormattingEdits(handle, model, range, options, token) }; + + return provider; } protected provideDocumentRangeFormattingEdits(handle: number, model: monaco.editor.ITextModel, diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index a88a36ada46e2..807a81827547c 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -385,7 +385,7 @@ export class LanguagesExtImpl implements LanguagesExt { registerDocumentRangeFormattingEditProvider(selector: theia.DocumentSelector, provider: theia.DocumentRangeFormattingEditProvider, pluginInfo: PluginInfo): theia.Disposable { const callId = this.addNewAdapter(new RangeFormattingAdapter(provider, this.documents)); - this.proxy.$registerRangeFormattingProvider(callId, pluginInfo, this.transformDocumentSelector(selector)); + this.proxy.$registerRangeFormattingSupport(callId, pluginInfo, this.transformDocumentSelector(selector)); return this.createDisposable(callId); } diff --git a/packages/plugin-metrics/src/browser/plugin-metrics-languages-main.ts b/packages/plugin-metrics/src/browser/plugin-metrics-languages-main.ts index caa6e06245baa..0d9416baa09c9 100644 --- a/packages/plugin-metrics/src/browser/plugin-metrics-languages-main.ts +++ b/packages/plugin-metrics/src/browser/plugin-metrics-languages-main.ts @@ -284,9 +284,9 @@ export class LanguagesMainPluginMetrics extends LanguagesMainImpl { super.$registerDocumentFormattingSupport(handle, pluginInfo, selector); } - $registerRangeFormattingProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { + $registerRangeFormattingSupport(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { this.registerPluginWithFeatureHandle(handle, pluginInfo.id); - super.$registerRangeFormattingProvider(handle, pluginInfo, selector); + super.$registerRangeFormattingSupport(handle, pluginInfo, selector); } $registerOnTypeFormattingProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], autoFormatTriggerCharacters: string[]): void {