diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 493a6ee340d4f2420633b54777fe677552b157d2..df4b5cdcd4fa84a447842eb1e5d18f3fcafac2b9 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -125,6 +125,7 @@ export async function formatDocumentRangesWithSelectedProvider( editorOrModel: ITextModel | IActiveCodeEditor, rangeOrRanges: Range | Range[], mode: FormattingMode, + progress: IProgress, token: CancellationToken ): Promise { @@ -133,6 +134,7 @@ export async function formatDocumentRangesWithSelectedProvider( const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const selected = await FormattingConflicts.select(provider, model, mode); if (selected) { + progress.report(selected); await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token); } } diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 5c7ab599ce7510dd928e19513af3395ff7cd4229..350aeef860e8780e077b5bd9089eb5981dba245f 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -202,7 +202,7 @@ class FormatOnPaste implements IEditorContribution { if (this.editor.getSelections().length > 1) { return; } - this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, CancellationToken.None).catch(onUnexpectedError); + this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, Progress.None, CancellationToken.None).catch(onUnexpectedError); } } @@ -274,7 +274,7 @@ class FormatSelectionAction extends EditorAction { const progressService = accessor.get(IEditorProgressService); await progressService.showWhile( - instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None), + instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, range, FormattingMode.Explicit, Progress.None, CancellationToken.None), 250 ); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 473166ff599a12e63b1152f6f61969b2f82b50ea..61c5b748882293a758ef0e4916dd2797bdc96b45 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import * as strings from 'vs/base/common/strings'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -13,11 +13,11 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeActionTriggerType, DocumentFormattingEditProvider, CodeActionProvider } from 'vs/editor/common/modes'; +import { CodeActionTriggerType, CodeActionProvider } from 'vs/editor/common/modes'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -29,6 +29,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { @@ -221,15 +223,14 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { if (!model.textEditorModel) { return; } + if (env.reason === SaveReason.AUTO) { + return undefined; + } const textEditorModel = model.textEditorModel; const overrides = { overrideIdentifier: textEditorModel.getLanguageIdentifier().language, resource: textEditorModel.uri }; - if (env.reason === SaveReason.AUTO || !this.configurationService.getValue('editor.formatOnSave', overrides)) { - return undefined; - } - - const nestedProgress = new Progress(provider => { + const nestedProgress = new Progress<{ displayName?: string, extensionId?: ExtensionIdentifier }>(provider => { progress.report({ message: localize( 'formatting', @@ -239,7 +240,19 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { }); }); const editorOrModel = findEditor(textEditorModel, this.codeEditorService) || textEditorModel; - await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + const config = this.configurationService.getValue('editor.formatOnSave', overrides); + + if (config === true || config === 'file') { + // format the whole file + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + + } else if (config === 'modifications') { + // format modifications + const ranges = await this.instantiationService.invokeFunction(getModifiedRanges, isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel); + if (ranges) { + await this.instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, editorOrModel, ranges, FormattingMode.Silent, nestedProgress, token); + } + } } } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 71f86a19285f4da388f8871596542b5f4ce56440..c4ee45a03623c57b7f675f08d1371a9d6efd7a58 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -360,8 +360,18 @@ configurationRegistry.registerConfiguration({ ...editorConfigurationBaseNode, properties: { 'editor.formatOnSave': { - 'type': 'boolean', - 'default': false, + 'type': 'string', + 'default': 'off', + 'enum': [ + 'off', + 'file', + 'modifications' + ], + 'enumDescriptions': [ + nls.localize('off', "Disable format on save."), + nls.localize('everything', "Format the whole file on save."), + nls.localize('modification', "Only format modifications (requires source control)."), + ], 'description': nls.localize('formatOnSave', "Format a file on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, } diff --git a/src/vs/workbench/contrib/format/browser/format.contribution.ts b/src/vs/workbench/contrib/format/browser/format.contribution.ts index 050ca89f5294879a1da7c685ca6cc5627ad74b21..a91827cdc4711410720ecd112f88fa32374b878b 100644 --- a/src/vs/workbench/contrib/format/browser/format.contribution.ts +++ b/src/vs/workbench/contrib/format/browser/format.contribution.ts @@ -5,4 +5,4 @@ import './formatActionsMultiple'; import './formatActionsNone'; -import './formatChanges'; +import './formatModified'; diff --git a/src/vs/workbench/contrib/format/browser/formatChanges.ts b/src/vs/workbench/contrib/format/browser/formatModified.ts similarity index 51% rename from src/vs/workbench/contrib/format/browser/formatChanges.ts rename to src/vs/workbench/contrib/format/browser/formatModified.ts index fd745b3bab38bc6f1018e8d08ba18f183a9433c1..e1ae53814d7ebc16291c1a99b642e299c5f01705 100644 --- a/src/vs/workbench/contrib/format/browser/formatChanges.ts +++ b/src/vs/workbench/contrib/format/browser/formatModified.ts @@ -5,84 +5,79 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { isEqualOrParent } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { formatDocumentRangesWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ISCMProvider, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { getOriginalResource } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; -registerEditorAction(class FormatChangedLinesAction extends EditorAction { +registerEditorAction(class FormatModifiedAction extends EditorAction { constructor() { super({ id: 'editor.action.formatChanges', - label: nls.localize('formatChanges', "Format Changed Lines"), - alias: 'Format Document...', + label: nls.localize('formatChanges', "Format Modified Lines"), + alias: 'Format Modified Lines', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), }); } async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const scmService = accessor.get(ISCMService); - const workerService = accessor.get(IEditorWorkerService); - const modelService = accessor.get(ITextModelService); const instaService = accessor.get(IInstantiationService); if (!editor.hasModel()) { return; } - const modified = editor.getModel().uri; - const provider = this._getBestProvider(scmService, modified); - if (!provider) { - return; - } - - const original = await provider.getOriginalResource(modified); - if (!original) { - return; - } - - const ranges: Range[] = []; - const ref = await modelService.createModelReference(original); - try { - if (workerService.canComputeDirtyDiff(original, modified)) { - const changes = await workerService.computeDirtyDiff(original, modified, true); - if (isNonEmptyArray(changes)) { - for (let change of changes) { - ranges.push(editor.getModel().validateRange(new Range( - change.modifiedStartLineNumber, 1, - change.modifiedEndLineNumber || change.modifiedStartLineNumber /*endLineNumber is 0 when things got deleted*/, Number.MAX_SAFE_INTEGER) - )); - } - } - } - } finally { - ref.dispose(); - } - - if (ranges.length > 0) { + const ranges = await instaService.invokeFunction(getModifiedRanges, editor.getModel()); + if (isNonEmptyArray(ranges)) { return instaService.invokeFunction( formatDocumentRangesWithSelectedProvider, editor, ranges, - FormattingMode.Explicit, CancellationToken.None + FormattingMode.Explicit, Progress.None, CancellationToken.None ); } } +}); - private _getBestProvider(scmService: ISCMService, uri: URI): ISCMProvider | undefined { - for (let repo of scmService.repositories) { - if (repo.provider.rootUri && isEqualOrParent(uri, repo.provider.rootUri)) { - return repo.provider; - } - } + +export async function getModifiedRanges(accessor: ServicesAccessor, modified: ITextModel): Promise { + const scmService = accessor.get(ISCMService); + const workerService = accessor.get(IEditorWorkerService); + const modelService = accessor.get(ITextModelService); + + const original = await getOriginalResource(scmService, modified.uri); + if (!original) { return undefined; } -}); + + const ranges: Range[] = []; + const ref = await modelService.createModelReference(original); + try { + if (!workerService.canComputeDirtyDiff(original, modified.uri)) { + return undefined; + } + const changes = await workerService.computeDirtyDiff(original, modified.uri, true); + if (!isNonEmptyArray(changes)) { + return undefined; + } + for (let change of changes) { + ranges.push(modified.validateRange(new Range( + change.modifiedStartLineNumber, 1, + change.modifiedEndLineNumber || change.modifiedStartLineNumber /*endLineNumber is 0 when things got deleted*/, Number.MAX_SAFE_INTEGER) + )); + } + } finally { + ref.dispose(); + } + + return ranges; +} diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 450448112bb0775f3ea97d384178c6d6d58a7ede..a4440db8e938c17b8c194883aeb67efc1fde4624 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -999,6 +999,22 @@ function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => }; } +export async function getOriginalResource(scmService: ISCMService, uri: URI): Promise { + const providers = scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (result) { + return result; + } + + const nonRootedProviders = providers.filter(p => !p.rootUri); + return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); +} + export class DirtyDiffModel extends Disposable { private _originalModel: IResolvedTextFileEditorModel | null = null; @@ -1155,19 +1171,7 @@ export class DirtyDiffModel extends Disposable { } const uri = this._model.resource; - const providers = this.scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); - - rootedProviders.sort(createProviderComparer(uri)); - - const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); - - if (result) { - return result; - } - - const nonRootedProviders = providers.filter(p => !p.rootUri); - return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); + return getOriginalResource(this.scmService, uri); } findNextClosestChange(lineNumber: number, inclusive = true): number {