提交 e4e0eb06 编写于 作者: J Johannes Rieken

add editor.defaultFormatter-setting, #71173

上级 9ce75053
......@@ -23,8 +23,8 @@ import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
import * as nls from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { ILabelService } from 'vs/platform/label/common/label';
import { IDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
export function alertFormattingEdits(edits: ISingleEditOperation[]): void {
......@@ -86,30 +86,51 @@ export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel):
return result;
}
export async function formatDocumentRangeWithFirstProvider(
export const enum FormattingMode {
Explicit = 1,
Silent = 2
}
export interface IFormattingEditProviderSelector {
<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined>;
}
export abstract class FormattingConflicts {
private static readonly _selectors = new LinkedList<IFormattingEditProviderSelector>();
static setFormatterSelector(selector: IFormattingEditProviderSelector): IDisposable {
const remove = FormattingConflicts._selectors.unshift(selector);
return { dispose: remove };
}
static async select<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> {
if (formatter.length === 0) {
return undefined;
}
const { value: selector } = FormattingConflicts._selectors.iterator().next();
if (selector) {
return await selector(formatter, document, mode);
}
return formatter[0];
}
}
export async function formatDocumentRangeWithSelectedProvider(
accessor: ServicesAccessor,
editorOrModel: ITextModel | IActiveCodeEditor,
range: Range,
mode: FormattingMode,
token: CancellationToken
): Promise<boolean> {
): Promise<void> {
const instaService = accessor.get(IInstantiationService);
const statusBarService = accessor.get(IStatusbarService);
const labelService = accessor.get(ILabelService);
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
const [best, ...rest] = DocumentRangeFormattingEditProviderRegistry.ordered(model);
if (!best) {
return false;
const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model);
const selected = await FormattingConflicts.select(provider, model, mode);
if (selected) {
await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token);
}
const ret = await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editorOrModel, range, token);
if (rest.length > 0) {
statusBarService.setStatusMessage(
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
5 * 1000
);
}
return ret;
}
export async function formatDocumentRangeWithProvider(
......@@ -181,29 +202,20 @@ export async function formatDocumentRangeWithProvider(
return true;
}
export async function formatDocumentWithFirstProvider(
export async function formatDocumentWithSelectedProvider(
accessor: ServicesAccessor,
editorOrModel: ITextModel | IActiveCodeEditor,
mode: FormattingMode,
token: CancellationToken
): Promise<boolean> {
): Promise<void> {
const instaService = accessor.get(IInstantiationService);
const statusBarService = accessor.get(IStatusbarService);
const labelService = accessor.get(ILabelService);
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
const [best, ...rest] = getRealAndSyntheticDocumentFormattersOrdered(model);
if (!best) {
return false;
}
const ret = await instaService.invokeFunction(formatDocumentWithProvider, best, editorOrModel, token);
if (rest.length > 0) {
statusBarService.setStatusMessage(
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
5 * 1000
);
const provider = getRealAndSyntheticDocumentFormattersOrdered(model);
const selected = await FormattingConflicts.select(provider, model, mode);
if (selected) {
await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, token);
}
return ret;
}
export async function formatDocumentWithProvider(
......
......@@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithFirstProvider, formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format';
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
import * as nls from 'vs/nls';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
......@@ -211,7 +211,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution {
if (this.editor.getSelections().length > 1) {
return;
}
this._instantiationService.invokeFunction(formatDocumentRangeWithFirstProvider, this.editor, range, CancellationToken.None).catch(onUnexpectedError);
this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Explicit, CancellationToken.None).catch(onUnexpectedError);
}
}
......@@ -240,7 +240,7 @@ class FormatDocumentAction extends EditorAction {
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
if (editor.hasModel()) {
const instaService = accessor.get(IInstantiationService);
await instaService.invokeFunction(formatDocumentWithFirstProvider, editor, CancellationToken.None);
await instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, CancellationToken.None);
}
}
}
......@@ -276,7 +276,7 @@ class FormatSelectionAction extends EditorAction {
if (range.isEmpty()) {
range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber));
}
await instaService.invokeFunction(formatDocumentRangeWithFirstProvider, editor, range, CancellationToken.None);
await instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None);
}
}
......
......@@ -21,7 +21,7 @@ import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
import { formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format';
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
......@@ -228,7 +228,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant {
return new Promise<any>((resolve, reject) => {
const source = new CancellationTokenSource();
const timeout = this._configurationService.getValue<number>('editor.formatOnSaveTimeout', overrides);
const request = this._instantiationService.invokeFunction(formatDocumentWithFirstProvider, model, source.token);
const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model, FormattingMode.Silent, source.token);
setTimeout(() => {
reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout));
......
......@@ -6,18 +6,138 @@
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
import { DocumentRangeFormattingEditProviderRegistry, DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'vs/editor/common/modes';
import * as nls from 'vs/nls';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IQuickInputService, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered } from 'vs/editor/contrib/format/format';
import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format';
import { Range } from 'vs/editor/common/core/range';
import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITextModel } from 'vs/editor/common/model';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IModeService } from 'vs/editor/common/services/modeService';
class DefaultFormatter extends Disposable implements IWorkbenchContribution {
static configName = 'editor.defaultFormatter';
static extensionIds: string[] = [];
static extensionDescriptions: string[] = [];
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IConfigurationService private readonly _configService: IConfigurationService,
@INotificationService private readonly _notificationService: INotificationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IModeService private readonly _modeService: IModeService,
) {
super();
this._register(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this));
this._register(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode)));
this._updateConfigValues();
}
private async _updateConfigValues(): Promise<void> {
const extensions = await this._extensionService.getExtensions();
DefaultFormatter.extensionIds.length = 0;
DefaultFormatter.extensionDescriptions.length = 0;
for (const extension of extensions) {
DefaultFormatter.extensionIds.push(extension.identifier.value);
DefaultFormatter.extensionDescriptions.push(extension.description || '');
}
}
private static _maybeQuotes(s: string): string {
return s.match(/\s/) ? `'${s}'` : s;
}
private async _selectFormatter<T extends DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> {
const defaultFormatterId = this._configService.getValue<string>(DefaultFormatter.configName, {
resource: document.uri,
overrideIdentifier: document.getModeId()
});
if (defaultFormatterId) {
// good -> formatter configured
const [defaultFormatter] = formatter.filter(formatter => formatter.extensionId && ExtensionIdentifier.equals(formatter.extensionId, defaultFormatterId));
if (defaultFormatter) {
// good -> formatter configured and available
return defaultFormatter;
}
}
const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId();
const message = defaultFormatterId
? nls.localize('config.bad', "The configured default formatter is not available. Select a different default formatter to continue.")
: nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName));
return new Promise<T | undefined>((resolve, reject) => {
this._notificationService.prompt(
Severity.Info,
message,
[{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }],
{ silent: mode === FormattingMode.Silent, onCancel: resolve }
);
if (mode === FormattingMode.Silent) {
// don't wait when formatting happens without interaction
// but pick some formatter...
resolve(formatter[0]);
}
});
}
private async _pickAndPersistDefaultFormatter<T extends DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider>(formatter: T[], document: ITextModel): Promise<T | undefined> {
const picks = formatter.map((formatter, index) => {
return <IIndexedPick>{
index,
label: formatter.displayName || formatter.extensionId || '?'
};
});
const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId();
const pick = await this._quickInputService.pick(picks, { placeHolder: nls.localize('select', "Select a default formatter for {0}-files", DefaultFormatter._maybeQuotes(langName)) });
if (!pick || !formatter[pick.index].extensionId) {
return undefined;
}
this._configService.updateValue(DefaultFormatter.configName, formatter[pick.index].extensionId!.value, {
resource: document.uri,
overrideIdentifier: document.getModeId()
});
return formatter[pick.index];
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(
DefaultFormatter,
LifecyclePhase.Restored
);
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'editor',
order: 5,
type: 'object',
overridable: true,
properties: {
[DefaultFormatter.configName]: {
description: nls.localize('formatter.default', "Defines a default formatter takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter."),
type: 'string',
enum: DefaultFormatter.extensionIds,
markdownEnumDescriptions: DefaultFormatter.extensionDescriptions
}
}
});
interface IIndexedPick extends IQuickPickItem {
index: number;
......@@ -69,25 +189,27 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction {
}
const instaService = accessor.get(IInstantiationService);
const quickPickService = accessor.get(IQuickInputService);
const viewletService = accessor.get(IViewletService);
const telemetryService = accessor.get(ITelemetryService);
const configService = accessor.get(IConfigurationService);
const model = editor.getModel();
const defaultFormatter = configService.getValue<string>(DefaultFormatter.configName, {
resource: model.uri,
overrideIdentifier: model.getModeId()
});
const provider = getRealAndSyntheticDocumentFormattersOrdered(model);
const picks = provider.map((provider, index) => {
return <IIndexedPick>{
index,
label: provider.displayName || '',
description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined,
buttons: [openExtensionAction]
};
});
const pick = await quickPickService.pick(picks, {
placeHolder: nls.localize('format.placeHolder', "Select a formatter"),
onDidTriggerItemButton: (e) => {
const { extensionId } = provider[e.item.index];
return showExtensionQuery(viewletService, `@id:${extensionId!.value}`);
}
placeHolder: nls.localize('format.placeHolder', "Select a formatter")
});
if (pick) {
await instaService.invokeFunction(formatDocumentWithProvider, provider[pick.index], editor, CancellationToken.None);
......@@ -119,9 +241,14 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction {
}
const instaService = accessor.get(IInstantiationService);
const quickPickService = accessor.get(IQuickInputService);
const viewletService = accessor.get(IViewletService);
const telemetryService = accessor.get(ITelemetryService);
const configService = accessor.get(IConfigurationService);
const model = editor.getModel();
const defaultFormatter = configService.getValue<string>(DefaultFormatter.configName, {
resource: model.uri,
overrideIdentifier: model.getModeId()
});
let range: Range = editor.getSelection();
if (range.isEmpty()) {
......@@ -133,16 +260,13 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction {
return <IIndexedPick>{
index,
label: provider.displayName || '',
description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined,
buttons: [openExtensionAction]
};
});
const pick = await quickPickService.pick(picks, {
placeHolder: nls.localize('format.placeHolder', "Select a formatter"),
onDidTriggerItemButton: (e) => {
const { extensionId } = provider[e.item.index];
return showExtensionQuery(viewletService, `@id:${extensionId!.value}`);
}
placeHolder: nls.localize('format.placeHolder', "Select a formatter")
});
if (pick) {
await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick.index], editor, range, CancellationToken.None);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册