Skip to content

Commit

Permalink
Add ability to set default formatter
Browse files Browse the repository at this point in the history
Signed-off-by: Vitaliy Gulyy <[email protected]>
  • Loading branch information
vitaliy-guliy committed Sep 15, 2020
1 parent 525b4e6 commit d664c3e
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 15 deletions.
9 changes: 7 additions & 2 deletions packages/editor/src/browser/editor-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
149 changes: 149 additions & 0 deletions packages/monaco/src/browser/monaco-formatting-conflicts.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
monaco.format.FormattingConflicts.setFormatterSelector(<T extends FormattingEditProvider>(
formatters: T[], document: monaco.editor.ITextModel, mode: monaco.format.FormattingMode) =>
this.selectFormatter(formatters, document, mode));
}

private async setDefaultFormatter(language: string, formatter: string): Promise<void> {
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<string>(name);
}

private async selectFormatter<T extends FormattingEditProvider>(
formatters: T[], document: monaco.editor.ITextModel, mode: monaco.format.FormattingMode): Promise<T | undefined> {

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<T> | undefined = new Deferred<T>();

const items: QuickOpenItem[] = formatters
.filter(formatter => formatter.displayName)
.map<QuickOpenItem>(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;
}

}
6 changes: 6 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)
Expand All @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions packages/monaco/src/browser/monaco-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function loadMonaco(vsRequire: any): Promise<void> {
'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',
Expand All @@ -86,6 +87,7 @@ export function loadMonaco(vsRequire: any): Promise<void> {
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,
Expand All @@ -109,6 +111,7 @@ export function loadMonaco(vsRequire: any): Promise<void> {
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;
Expand Down
41 changes: 41 additions & 0 deletions packages/monaco/src/typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/// <reference types='@theia/monaco-editor-core/monaco'/>

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 {
<T extends (monaco.languages.DocumentFormattingEditProvider | monaco.languages.DocumentRangeFormattingEditProvider)>(formatter: T[], document: monaco.editor.ITextModel, mode: FormattingMode): Promise<T | undefined>;
}

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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 23 additions & 9 deletions packages/plugin-ext/src/main/browser/languages-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,31 +562,45 @@ 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,
options: monaco.languages.FormattingOptions, token: monaco.CancellationToken): monaco.languages.ProviderResult<monaco.languages.TextEdit[]> {
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,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/plugin/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d664c3e

Please sign in to comment.