diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index ca0bb68c6bd..25e78075d34 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1213,6 +1213,8 @@ export interface DocumentRangeFormattingEditProvider { readonly displayName?: string; + readonly multiRange: boolean; + /** * Provide formatting edits for a range in a document. * @@ -1220,7 +1222,7 @@ export interface DocumentRangeFormattingEditProvider { * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. */ - provideDocumentRangeFormattingEdits(model: model.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult; + provideDocumentRangeFormattingEdits(model: model.ITextModel, range: Range | Range[], options: FormattingOptions, token: CancellationToken): ProviderResult; } /** * The document formatting provider interface defines the contract between extensions and diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index 23b3967d26a..97bee12cd9a 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -107,6 +107,11 @@ export interface IFormattingEditProviderSelector { (formatter: T[], document: ITextModel, mode: FormattingMode): Promise; } +interface IEditResult { + cancelled: boolean; + allEdits: TextEdit[]; +} + export abstract class FormattingConflicts { private static readonly _selectors = new LinkedList(); @@ -167,7 +172,95 @@ export async function formatDocumentRangesWithProvider( model = editorOrModel; cts = new TextModelCancellationTokenSource(editorOrModel, token); } + let result: IEditResult; + if (provider.multiRange) { + result = await formatDocumentRangesWithMultiRangeProvider(provider, editorOrModel, rangeOrRanges, cts, model, workerService); + } else { + result = await formatDocumentRangesWithSingleRangeProvider(provider, editorOrModel, rangeOrRanges, cts, model, workerService); + } + if (result.cancelled) { return true; } + const { allEdits } = result; + + if (allEdits.length === 0) { + return false; + } + + if (isCodeEditor(editorOrModel)) { + // use editor to apply edits + FormattingEdit.execute(editorOrModel, allEdits, true); + alertFormattingEdits(allEdits); + editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); + + } else { + // use model to apply edits + const [{ range }] = allEdits; + const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + model.pushEditOperations([initialSelection], allEdits.map(edit => { + return { + text: edit.text, + range: Range.lift(edit.range), + forceMoveMarkers: true + }; + }), undoEdits => { + for (const { range } of undoEdits) { + if (Range.areIntersectingOrTouching(range, initialSelection)) { + return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)]; + } + } + return null; + }); + } + + return true; +} + +async function formatDocumentRangesWithMultiRangeProvider( + provider: DocumentRangeFormattingEditProvider, + editorOrModel: ITextModel | IActiveCodeEditor, + rangeOrRanges: Range | Range[], + cts: CancellationTokenSource, + model: ITextModel, + workerService: IEditorWorkerService +): Promise { + const ranges = asArray(rangeOrRanges); + + const allEdits: TextEdit[] = []; + try { + if (cts.token.isCancellationRequested) { + return { cancelled: true, allEdits }; + } + + const rawEdits = (await provider.provideDocumentRangeFormattingEdits( + model, + ranges, + model.getFormattingOptions(), + cts.token + )) || []; + + if (cts.token.isCancellationRequested) { + return { cancelled: true, allEdits }; + } + + const minimalEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + if (minimalEdits) { + allEdits.push(...minimalEdits); + } + } finally { + cts.dispose(); + } + + return { cancelled: false, allEdits }; +} + +async function formatDocumentRangesWithSingleRangeProvider( + provider: DocumentRangeFormattingEditProvider, + editorOrModel: ITextModel | IActiveCodeEditor, + rangeOrRanges: Range | Range[], + cts: CancellationTokenSource, + model: ITextModel, + workerService: IEditorWorkerService +): Promise { // make sure that ranges don't overlap nor touch each other const ranges: Range[] = []; let len = 0; @@ -219,7 +312,7 @@ export async function formatDocumentRangesWithProvider( try { for (const range of ranges) { if (cts.token.isCancellationRequested) { - return true; + return { cancelled: true, allEdits }; } rawEditsList.push(await computeEdits(range)); } @@ -227,7 +320,7 @@ export async function formatDocumentRangesWithProvider( for (let i = 0; i < ranges.length; ++i) { for (let j = i + 1; j < ranges.length; ++j) { if (cts.token.isCancellationRequested) { - return true; + return { cancelled: true, allEdits }; } if (hasIntersectingEdit(rawEditsList[i], rawEditsList[j])) { // Merge ranges i and j into a single range, recompute the associated edits @@ -248,7 +341,7 @@ export async function formatDocumentRangesWithProvider( for (const rawEdits of rawEditsList) { if (cts.token.isCancellationRequested) { - return true; + return { cancelled: true, allEdits }; } const minimalEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); if (minimalEdits) { @@ -258,38 +351,7 @@ export async function formatDocumentRangesWithProvider( } finally { cts.dispose(); } - - if (allEdits.length === 0) { - return false; - } - - if (isCodeEditor(editorOrModel)) { - // use editor to apply edits - FormattingEdit.execute(editorOrModel, allEdits, true); - alertFormattingEdits(allEdits); - editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); - - } else { - // use model to apply edits - const [{ range }] = allEdits; - const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - model.pushEditOperations([initialSelection], allEdits.map(edit => { - return { - text: edit.text, - range: Range.lift(edit.range), - forceMoveMarkers: true - }; - }), undoEdits => { - for (const { range } of undoEdits) { - if (Range.areIntersectingOrTouching(range, initialSelection)) { - return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)]; - } - } - return null; - }); - } - - return true; + return { cancelled: false, allEdits }; } export async function formatDocumentWithSelectedProvider( diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 27d0f7945ec..9544913da5e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7141,6 +7141,7 @@ declare namespace monaco.languages { */ export interface DocumentRangeFormattingEditProvider { readonly displayName?: string; + readonly multiRange: boolean; /** * Provide formatting edits for a range in a document. * @@ -7148,7 +7149,7 @@ declare namespace monaco.languages { * or larger range. Often this is done by adjusting the start and end * of the range to full syntax nodes. */ - provideDocumentRangeFormattingEdits(model: editor.ITextModel, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult; + provideDocumentRangeFormattingEdits(model: editor.ITextModel, range: Range | Range[], options: FormattingOptions, token: CancellationToken): ProviderResult; } /** diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 005cf3ea496..1ffd2600354 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -32,7 +32,7 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, IRangeFormattingProviderMetadataDto } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -387,11 +387,12 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread })); } - $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void { + $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, metadata: IRangeFormattingProviderMetadataDto): void { this._registrations.set(handle, this._languageFeaturesService.documentRangeFormattingEditProvider.register(selector, { extensionId, displayName, - provideDocumentRangeFormattingEdits: (model: ITextModel, range: EditorRange, options: languages.FormattingOptions, token: CancellationToken): Promise => { + multiRange: metadata.multiRange, + provideDocumentRangeFormattingEdits: (model: ITextModel, range: EditorRange | EditorRange[], options: languages.FormattingOptions, token: CancellationToken): Promise => { return this._proxy.$provideDocumentRangeFormattingEdits(handle, model.uri, range, options, token); } })); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8886bf9d058..65363f34c8e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -526,8 +526,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerDocumentFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentFormattingEditProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentFormattingEditProvider(extension, checkSelector(selector), provider); }, - registerDocumentRangeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider): vscode.Disposable { - return extHostLanguageFeatures.registerDocumentRangeFormattingEditProvider(extension, checkSelector(selector), provider); + registerDocumentRangeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider, metadata?: vscode.DocumentRangeFormattingEditProviderMetadata): vscode.Disposable { + return extHostLanguageFeatures.registerDocumentRangeFormattingEditProvider(extension, checkSelector(selector), provider, metadata); }, registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(extension, checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index db3d3eccec1..f8a2db1692c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -352,6 +352,10 @@ export interface IDocumentFilterDto { notebookType?: string; } +export interface IRangeFormattingProviderMetadataDto { + readonly multiRange?: boolean; +} + export interface ISignatureHelpProviderMetadataDto { readonly triggerCharacters: readonly string[]; readonly retriggerCharacters: readonly string[]; @@ -384,7 +388,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean, pasteMimeTypes: readonly string[]): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; - $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; + $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, metadata: IRangeFormattingProviderMetadataDto): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; $registerNavigateTypeSupport(handle: number, supportsResolve: boolean): void; $registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void; @@ -1771,7 +1775,7 @@ export interface ExtHostLanguageFeaturesShape { $prepareDocumentPaste(handle: number, uri: UriComponents, ranges: readonly IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; $providePasteEdits(handle: number, requestId: number, uri: UriComponents, ranges: IRange[], dataTransfer: DataTransferDTO, token: CancellationToken): Promise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise; - $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise; + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange | IRange[], options: languages.FormattingOptions, token: CancellationToken): Promise; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideWorkspaceSymbols(handle: number, search: string, token: CancellationToken): Promise; $resolveWorkspaceSymbol(handle: number, symbol: IWorkspaceSymbolDto, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 087fb2df70c..201a29ec75d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -547,19 +547,27 @@ class DocumentFormattingAdapter { } } -class RangeFormattingAdapter { +class RangeFormattingAdapter { constructor( private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.DocumentRangeFormattingEditProvider + private readonly _provider: vscode.DocumentRangeFormattingEditProvider, + private readonly _multiRange: boolean ) { } - async provideDocumentRangeFormattingEdits(resource: URI, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise { + async provideDocumentRangeFormattingEdits(resource: URI, range: IRange | IRange[], options: languages.FormattingOptions, token: CancellationToken): Promise { const document = this._documents.getDocument(resource); - const ran = typeConvert.Range.to(range); + let ran: Range | Range[]; + if (this._multiRange) { + if (!Array.isArray(range)) { range = [range]; } + ran = range.map(typeConvert.Range.to); + } else { + if (Array.isArray(range)) { throw new Error('Provided list of ranges to non-multiRange provider'); } + ran = typeConvert.Range.to(range); + } - const value = await this._provider.provideDocumentRangeFormattingEdits(document, ran, options, token); + const value = await this._provider.provideDocumentRangeFormattingEdits(document, ran, options, token); if (Array.isArray(value)) { return value.map(typeConvert.TextEdit.from); } @@ -1722,9 +1730,9 @@ class DocumentOnDropEditAdapter { } } -type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter +type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter - | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter + | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter @@ -1815,7 +1823,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return result; } - private _addNewAdapter(adapter: Adapter, extension: IExtensionDescription): number { + private _addNewAdapter(adapter: Adapter, extension: IExtensionDescription): number { const handle = this._nextHandle(); this._adapter.set(handle, new AdapterData(adapter, extension)); return handle; @@ -2041,13 +2049,13 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentFormattingAdapter, adapter => adapter.provideDocumentFormattingEdits(URI.revive(resource), options, token), undefined, token); } - registerDocumentRangeFormattingEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider): vscode.Disposable { - const handle = this._addNewAdapter(new RangeFormattingAdapter(this._documents, provider), extension); - this._proxy.$registerRangeFormattingSupport(handle, this._transformDocumentSelector(selector), extension.identifier, extension.displayName || extension.name); + registerDocumentRangeFormattingEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentRangeFormattingEditProvider, metadata?: vscode.DocumentRangeFormattingEditProviderMetadata): vscode.Disposable { + const handle = this._addNewAdapter(new RangeFormattingAdapter(this._documents, provider, metadata?.multiRange ?? false), extension); + this._proxy.$registerRangeFormattingSupport(handle, this._transformDocumentSelector(selector), extension.identifier, extension.displayName || extension.name, metadata ?? { multiRange: false }); return this._createDisposable(handle); } - $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise { + $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange | IRange[], options: languages.FormattingOptions, token: CancellationToken): Promise { return this._withAdapter(handle, RangeFormattingAdapter, adapter => adapter.provideDocumentRangeFormattingEdits(URI.revive(resource), range, options, token), undefined, token); } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 7bdd9fefda4..0a100f664dd 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -4117,7 +4117,7 @@ declare module 'vscode' { * The document formatting provider interface defines the contract between extensions and * the formatting-feature. */ - export interface DocumentRangeFormattingEditProvider { + export interface DocumentRangeFormattingEditProvider { /** * Provide formatting edits for a range in a document. @@ -4127,13 +4127,23 @@ declare module 'vscode' { * of the range to full syntax nodes. * * @param document The document in which the command was invoked. - * @param range The range which should be formatted. + * @param range The range or ranges which should be formatted. * @param options Options controlling formatting. * @param token A cancellation token. * @return A set of text edits or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult; + provideDocumentRangeFormattingEdits(document: TextDocument, range: T, options: FormattingOptions, token: CancellationToken): ProviderResult; + } + + /** + * Metadata about a registered {@linkcode DocumentRangeFormattingEditProvider}. + */ + export interface DocumentRangeFormattingEditProviderMetadata { + /** + * `true` if the range formatting provider supports formatting multiple ranges at once. + */ + readonly multiRange?: boolean; } /** @@ -13078,9 +13088,10 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A document range formatting edit provider. + * @param metadata Metadata about the provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ - export function registerDocumentRangeFormattingEditProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider): Disposable; + export function registerDocumentRangeFormattingEditProvider(selector: DocumentSelector, provider: DocumentRangeFormattingEditProvider, metadata?: DocumentRangeFormattingEditProviderMetadata): Disposable; /** * Register a formatting provider that works on type. The provider is active when the user enables the setting `editor.formatOnType`.