Skip to content

Commit e4e0eb0

Browse files
committed
add editor.defaultFormatter-setting, microsoft#71173
1 parent 9ce7505 commit e4e0eb0

4 files changed

Lines changed: 192 additions & 56 deletions

File tree

src/vs/editor/contrib/format/format.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
2323
import * as nls from 'vs/nls';
2424
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
2525
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
26-
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
27-
import { ILabelService } from 'vs/platform/label/common/label';
26+
import { IDisposable } from 'vs/base/common/lifecycle';
27+
import { LinkedList } from 'vs/base/common/linkedList';
2828

2929
export function alertFormattingEdits(edits: ISingleEditOperation[]): void {
3030

@@ -86,30 +86,51 @@ export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel):
8686
return result;
8787
}
8888

89-
export async function formatDocumentRangeWithFirstProvider(
89+
export const enum FormattingMode {
90+
Explicit = 1,
91+
Silent = 2
92+
}
93+
94+
export interface IFormattingEditProviderSelector {
95+
<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined>;
96+
}
97+
98+
export abstract class FormattingConflicts {
99+
100+
private static readonly _selectors = new LinkedList<IFormattingEditProviderSelector>();
101+
102+
static setFormatterSelector(selector: IFormattingEditProviderSelector): IDisposable {
103+
const remove = FormattingConflicts._selectors.unshift(selector);
104+
return { dispose: remove };
105+
}
106+
107+
static async select<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> {
108+
if (formatter.length === 0) {
109+
return undefined;
110+
}
111+
const { value: selector } = FormattingConflicts._selectors.iterator().next();
112+
if (selector) {
113+
return await selector(formatter, document, mode);
114+
}
115+
return formatter[0];
116+
}
117+
}
118+
119+
export async function formatDocumentRangeWithSelectedProvider(
90120
accessor: ServicesAccessor,
91121
editorOrModel: ITextModel | IActiveCodeEditor,
92122
range: Range,
123+
mode: FormattingMode,
93124
token: CancellationToken
94-
): Promise<boolean> {
125+
): Promise<void> {
95126

96127
const instaService = accessor.get(IInstantiationService);
97-
const statusBarService = accessor.get(IStatusbarService);
98-
const labelService = accessor.get(ILabelService);
99-
100128
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
101-
const [best, ...rest] = DocumentRangeFormattingEditProviderRegistry.ordered(model);
102-
if (!best) {
103-
return false;
129+
const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model);
130+
const selected = await FormattingConflicts.select(provider, model, mode);
131+
if (selected) {
132+
await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token);
104133
}
105-
const ret = await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editorOrModel, range, token);
106-
if (rest.length > 0) {
107-
statusBarService.setStatusMessage(
108-
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
109-
5 * 1000
110-
);
111-
}
112-
return ret;
113134
}
114135

115136
export async function formatDocumentRangeWithProvider(
@@ -181,29 +202,20 @@ export async function formatDocumentRangeWithProvider(
181202
return true;
182203
}
183204

184-
export async function formatDocumentWithFirstProvider(
205+
export async function formatDocumentWithSelectedProvider(
185206
accessor: ServicesAccessor,
186207
editorOrModel: ITextModel | IActiveCodeEditor,
208+
mode: FormattingMode,
187209
token: CancellationToken
188-
): Promise<boolean> {
210+
): Promise<void> {
189211

190212
const instaService = accessor.get(IInstantiationService);
191-
const statusBarService = accessor.get(IStatusbarService);
192-
const labelService = accessor.get(ILabelService);
193-
194213
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
195-
const [best, ...rest] = getRealAndSyntheticDocumentFormattersOrdered(model);
196-
if (!best) {
197-
return false;
198-
}
199-
const ret = await instaService.invokeFunction(formatDocumentWithProvider, best, editorOrModel, token);
200-
if (rest.length > 0) {
201-
statusBarService.setStatusMessage(
202-
nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName),
203-
5 * 1000
204-
);
214+
const provider = getRealAndSyntheticDocumentFormattersOrdered(model);
215+
const selected = await FormattingConflicts.select(provider, model, mode);
216+
if (selected) {
217+
await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, token);
205218
}
206-
return ret;
207219
}
208220

209221
export async function formatDocumentWithProvider(

src/vs/editor/contrib/format/formatActions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon';
1616
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
1717
import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
1818
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
19-
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithFirstProvider, formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format';
19+
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
2020
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
2121
import * as nls from 'vs/nls';
2222
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
@@ -211,7 +211,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution {
211211
if (this.editor.getSelections().length > 1) {
212212
return;
213213
}
214-
this._instantiationService.invokeFunction(formatDocumentRangeWithFirstProvider, this.editor, range, CancellationToken.None).catch(onUnexpectedError);
214+
this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Explicit, CancellationToken.None).catch(onUnexpectedError);
215215
}
216216
}
217217

@@ -240,7 +240,7 @@ class FormatDocumentAction extends EditorAction {
240240
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
241241
if (editor.hasModel()) {
242242
const instaService = accessor.get(IInstantiationService);
243-
await instaService.invokeFunction(formatDocumentWithFirstProvider, editor, CancellationToken.None);
243+
await instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, CancellationToken.None);
244244
}
245245
}
246246
}
@@ -276,7 +276,7 @@ class FormatSelectionAction extends EditorAction {
276276
if (range.isEmpty()) {
277277
range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber));
278278
}
279-
await instaService.invokeFunction(formatDocumentRangeWithFirstProvider, editor, range, CancellationToken.None);
279+
await instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None);
280280
}
281281
}
282282

src/vs/workbench/api/browser/mainThreadSaveParticipant.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
2121
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
2222
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
2323
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
24-
import { formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format';
24+
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
2525
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
2626
import { localize } from 'vs/nls';
2727
import { ICommandService } from 'vs/platform/commands/common/commands';
@@ -228,7 +228,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant {
228228
return new Promise<any>((resolve, reject) => {
229229
const source = new CancellationTokenSource();
230230
const timeout = this._configurationService.getValue<number>('editor.formatOnSaveTimeout', overrides);
231-
const request = this._instantiationService.invokeFunction(formatDocumentWithFirstProvider, model, source.token);
231+
const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model, FormattingMode.Silent, source.token);
232232

233233
setTimeout(() => {
234234
reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout));

src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,138 @@
66
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
77
import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
88
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
9-
import { DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
9+
import { DocumentRangeFormattingEditProviderRegistry, DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'vs/editor/common/modes';
1010
import * as nls from 'vs/nls';
1111
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
1212
import { IQuickInputService, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput';
1313
import { CancellationToken } from 'vs/base/common/cancellation';
1414
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
15-
import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered } from 'vs/editor/contrib/format/format';
15+
import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format';
1616
import { Range } from 'vs/editor/common/core/range';
17-
import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery';
18-
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
1917
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2018
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
19+
import { Registry } from 'vs/platform/registry/common/platform';
20+
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
21+
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
22+
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
23+
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
24+
import { Disposable } from 'vs/base/common/lifecycle';
25+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
26+
import { ITextModel } from 'vs/editor/common/model';
27+
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
28+
import { IModeService } from 'vs/editor/common/services/modeService';
29+
30+
class DefaultFormatter extends Disposable implements IWorkbenchContribution {
31+
32+
static configName = 'editor.defaultFormatter';
33+
34+
static extensionIds: string[] = [];
35+
static extensionDescriptions: string[] = [];
36+
37+
constructor(
38+
@IExtensionService private readonly _extensionService: IExtensionService,
39+
@IConfigurationService private readonly _configService: IConfigurationService,
40+
@INotificationService private readonly _notificationService: INotificationService,
41+
@IQuickInputService private readonly _quickInputService: IQuickInputService,
42+
@IModeService private readonly _modeService: IModeService,
43+
) {
44+
super();
45+
this._register(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this));
46+
this._register(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode)));
47+
this._updateConfigValues();
48+
}
49+
50+
private async _updateConfigValues(): Promise<void> {
51+
const extensions = await this._extensionService.getExtensions();
52+
53+
DefaultFormatter.extensionIds.length = 0;
54+
DefaultFormatter.extensionDescriptions.length = 0;
55+
for (const extension of extensions) {
56+
DefaultFormatter.extensionIds.push(extension.identifier.value);
57+
DefaultFormatter.extensionDescriptions.push(extension.description || '');
58+
}
59+
}
60+
61+
private static _maybeQuotes(s: string): string {
62+
return s.match(/\s/) ? `'${s}'` : s;
63+
}
64+
65+
private async _selectFormatter<T extends DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> {
66+
67+
const defaultFormatterId = this._configService.getValue<string>(DefaultFormatter.configName, {
68+
resource: document.uri,
69+
overrideIdentifier: document.getModeId()
70+
});
71+
72+
if (defaultFormatterId) {
73+
// good -> formatter configured
74+
const [defaultFormatter] = formatter.filter(formatter => formatter.extensionId && ExtensionIdentifier.equals(formatter.extensionId, defaultFormatterId));
75+
if (defaultFormatter) {
76+
// good -> formatter configured and available
77+
return defaultFormatter;
78+
}
79+
}
80+
81+
const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId();
82+
const message = defaultFormatterId
83+
? nls.localize('config.bad', "The configured default formatter is not available. Select a different default formatter to continue.")
84+
: nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName));
85+
86+
return new Promise<T | undefined>((resolve, reject) => {
87+
this._notificationService.prompt(
88+
Severity.Info,
89+
message,
90+
[{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }],
91+
{ silent: mode === FormattingMode.Silent, onCancel: resolve }
92+
);
93+
94+
if (mode === FormattingMode.Silent) {
95+
// don't wait when formatting happens without interaction
96+
// but pick some formatter...
97+
resolve(formatter[0]);
98+
}
99+
});
100+
}
101+
102+
private async _pickAndPersistDefaultFormatter<T extends DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider>(formatter: T[], document: ITextModel): Promise<T | undefined> {
103+
const picks = formatter.map((formatter, index) => {
104+
return <IIndexedPick>{
105+
index,
106+
label: formatter.displayName || formatter.extensionId || '?'
107+
};
108+
});
109+
const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId();
110+
const pick = await this._quickInputService.pick(picks, { placeHolder: nls.localize('select', "Select a default formatter for {0}-files", DefaultFormatter._maybeQuotes(langName)) });
111+
if (!pick || !formatter[pick.index].extensionId) {
112+
return undefined;
113+
}
114+
this._configService.updateValue(DefaultFormatter.configName, formatter[pick.index].extensionId!.value, {
115+
resource: document.uri,
116+
overrideIdentifier: document.getModeId()
117+
});
118+
return formatter[pick.index];
119+
}
120+
}
121+
122+
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(
123+
DefaultFormatter,
124+
LifecyclePhase.Restored
125+
);
126+
127+
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
128+
id: 'editor',
129+
order: 5,
130+
type: 'object',
131+
overridable: true,
132+
properties: {
133+
[DefaultFormatter.configName]: {
134+
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."),
135+
type: 'string',
136+
enum: DefaultFormatter.extensionIds,
137+
markdownEnumDescriptions: DefaultFormatter.extensionDescriptions
138+
}
139+
}
140+
});
21141

22142
interface IIndexedPick extends IQuickPickItem {
23143
index: number;
@@ -69,25 +189,27 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction {
69189
}
70190
const instaService = accessor.get(IInstantiationService);
71191
const quickPickService = accessor.get(IQuickInputService);
72-
const viewletService = accessor.get(IViewletService);
73192
const telemetryService = accessor.get(ITelemetryService);
193+
const configService = accessor.get(IConfigurationService);
194+
74195
const model = editor.getModel();
196+
const defaultFormatter = configService.getValue<string>(DefaultFormatter.configName, {
197+
resource: model.uri,
198+
overrideIdentifier: model.getModeId()
199+
});
75200

76201
const provider = getRealAndSyntheticDocumentFormattersOrdered(model);
77202
const picks = provider.map((provider, index) => {
78203
return <IIndexedPick>{
79204
index,
80205
label: provider.displayName || '',
206+
description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined,
81207
buttons: [openExtensionAction]
82208
};
83209
});
84210

85211
const pick = await quickPickService.pick(picks, {
86-
placeHolder: nls.localize('format.placeHolder', "Select a formatter"),
87-
onDidTriggerItemButton: (e) => {
88-
const { extensionId } = provider[e.item.index];
89-
return showExtensionQuery(viewletService, `@id:${extensionId!.value}`);
90-
}
212+
placeHolder: nls.localize('format.placeHolder', "Select a formatter")
91213
});
92214
if (pick) {
93215
await instaService.invokeFunction(formatDocumentWithProvider, provider[pick.index], editor, CancellationToken.None);
@@ -119,9 +241,14 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction {
119241
}
120242
const instaService = accessor.get(IInstantiationService);
121243
const quickPickService = accessor.get(IQuickInputService);
122-
const viewletService = accessor.get(IViewletService);
123244
const telemetryService = accessor.get(ITelemetryService);
245+
const configService = accessor.get(IConfigurationService);
246+
124247
const model = editor.getModel();
248+
const defaultFormatter = configService.getValue<string>(DefaultFormatter.configName, {
249+
resource: model.uri,
250+
overrideIdentifier: model.getModeId()
251+
});
125252

126253
let range: Range = editor.getSelection();
127254
if (range.isEmpty()) {
@@ -133,16 +260,13 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction {
133260
return <IIndexedPick>{
134261
index,
135262
label: provider.displayName || '',
263+
description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined,
136264
buttons: [openExtensionAction]
137265
};
138266
});
139267

140268
const pick = await quickPickService.pick(picks, {
141-
placeHolder: nls.localize('format.placeHolder', "Select a formatter"),
142-
onDidTriggerItemButton: (e) => {
143-
const { extensionId } = provider[e.item.index];
144-
return showExtensionQuery(viewletService, `@id:${extensionId!.value}`);
145-
}
269+
placeHolder: nls.localize('format.placeHolder', "Select a formatter")
146270
});
147271
if (pick) {
148272
await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick.index], editor, range, CancellationToken.None);

0 commit comments

Comments
 (0)