mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 10:38:31 -05:00
231 lines
7.6 KiB
TypeScript
231 lines
7.6 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { equals, flatten, isNonEmptyArray, mergeSort, coalesce } from 'vs/base/common/arrays';
|
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
|
import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors';
|
|
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState';
|
|
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
|
import { Range } from 'vs/editor/common/core/range';
|
|
import { Selection } from 'vs/editor/common/core/selection';
|
|
import { ITextModel } from 'vs/editor/common/model';
|
|
import * as modes from 'vs/editor/common/modes';
|
|
import { IModelService } from 'vs/editor/common/services/modelService';
|
|
import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './types';
|
|
import { IProgress, Progress } from 'vs/platform/progress/common/progress';
|
|
|
|
export const codeActionCommandId = 'editor.action.codeAction';
|
|
export const refactorCommandId = 'editor.action.refactor';
|
|
export const sourceActionCommandId = 'editor.action.sourceAction';
|
|
export const organizeImportsCommandId = 'editor.action.organizeImports';
|
|
export const fixAllCommandId = 'editor.action.fixAll';
|
|
|
|
export interface CodeActionSet extends IDisposable {
|
|
readonly validActions: readonly modes.CodeAction[];
|
|
readonly allActions: readonly modes.CodeAction[];
|
|
readonly hasAutoFix: boolean;
|
|
|
|
readonly documentation: readonly modes.Command[];
|
|
}
|
|
|
|
class ManagedCodeActionSet extends Disposable implements CodeActionSet {
|
|
|
|
private static codeActionsComparator(a: modes.CodeAction, b: modes.CodeAction): number {
|
|
if (a.isPreferred && !b.isPreferred) {
|
|
return -1;
|
|
} else if (!a.isPreferred && b.isPreferred) {
|
|
return 1;
|
|
}
|
|
|
|
if (isNonEmptyArray(a.diagnostics)) {
|
|
if (isNonEmptyArray(b.diagnostics)) {
|
|
return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message);
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else if (isNonEmptyArray(b.diagnostics)) {
|
|
return 1;
|
|
} else {
|
|
return 0; // both have no diagnostics
|
|
}
|
|
}
|
|
|
|
public readonly validActions: readonly modes.CodeAction[];
|
|
public readonly allActions: readonly modes.CodeAction[];
|
|
|
|
public constructor(
|
|
actions: readonly modes.CodeAction[],
|
|
public readonly documentation: readonly modes.Command[],
|
|
disposables: DisposableStore,
|
|
) {
|
|
super();
|
|
this._register(disposables);
|
|
this.allActions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator);
|
|
this.validActions = this.allActions.filter(action => !action.disabled);
|
|
}
|
|
|
|
public get hasAutoFix() {
|
|
return this.validActions.some(fix => !!fix.kind && CodeActionKind.QuickFix.contains(new CodeActionKind(fix.kind)) && !!fix.isPreferred);
|
|
}
|
|
}
|
|
|
|
|
|
const emptyCodeActionsResponse = { actions: [] as modes.CodeAction[], documentation: undefined };
|
|
|
|
export function getCodeActions(
|
|
model: ITextModel,
|
|
rangeOrSelection: Range | Selection,
|
|
trigger: CodeActionTrigger,
|
|
progress: IProgress<modes.CodeActionProvider>,
|
|
token: CancellationToken,
|
|
): Promise<CodeActionSet> {
|
|
const filter = trigger.filter || {};
|
|
|
|
const codeActionContext: modes.CodeActionContext = {
|
|
only: filter.include?.value,
|
|
trigger: trigger.type,
|
|
};
|
|
|
|
const cts = new TextModelCancellationTokenSource(model, token);
|
|
const providers = getCodeActionProviders(model, filter);
|
|
|
|
const disposables = new DisposableStore();
|
|
const promises = providers.map(async provider => {
|
|
try {
|
|
progress.report(provider);
|
|
const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token);
|
|
if (providedCodeActions) {
|
|
disposables.add(providedCodeActions);
|
|
}
|
|
|
|
if (cts.token.isCancellationRequested) {
|
|
return emptyCodeActionsResponse;
|
|
}
|
|
|
|
const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action));
|
|
const documentation = getDocumentation(provider, filteredActions, filter.include);
|
|
return { actions: filteredActions, documentation };
|
|
} catch (err) {
|
|
if (isPromiseCanceledError(err)) {
|
|
throw err;
|
|
}
|
|
onUnexpectedExternalError(err);
|
|
return emptyCodeActionsResponse;
|
|
}
|
|
});
|
|
|
|
const listener = modes.CodeActionProviderRegistry.onDidChange(() => {
|
|
const newProviders = modes.CodeActionProviderRegistry.all(model);
|
|
if (!equals(newProviders, providers)) {
|
|
cts.cancel();
|
|
}
|
|
});
|
|
|
|
return Promise.all(promises).then(actions => {
|
|
const allActions = flatten(actions.map(x => x.actions));
|
|
const allDocumentation = coalesce(actions.map(x => x.documentation));
|
|
return new ManagedCodeActionSet(allActions, allDocumentation, disposables);
|
|
})
|
|
.finally(() => {
|
|
listener.dispose();
|
|
cts.dispose();
|
|
});
|
|
}
|
|
|
|
function getCodeActionProviders(
|
|
model: ITextModel,
|
|
filter: CodeActionFilter
|
|
) {
|
|
return modes.CodeActionProviderRegistry.all(model)
|
|
// Don't include providers that we know will not return code actions of interest
|
|
.filter(provider => {
|
|
if (!provider.providedCodeActionKinds) {
|
|
// We don't know what type of actions this provider will return.
|
|
return true;
|
|
}
|
|
return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind)));
|
|
});
|
|
}
|
|
|
|
function getDocumentation(
|
|
provider: modes.CodeActionProvider,
|
|
providedCodeActions: readonly modes.CodeAction[],
|
|
only?: CodeActionKind
|
|
): modes.Command | undefined {
|
|
if (!provider.documentation) {
|
|
return undefined;
|
|
}
|
|
|
|
const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command }));
|
|
|
|
if (only) {
|
|
let currentBest: { readonly kind: CodeActionKind, readonly command: modes.Command } | undefined;
|
|
for (const entry of documentation) {
|
|
if (entry.kind.contains(only)) {
|
|
if (!currentBest) {
|
|
currentBest = entry;
|
|
} else {
|
|
// Take best match
|
|
if (currentBest.kind.contains(entry.kind)) {
|
|
currentBest = entry;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (currentBest) {
|
|
return currentBest?.command;
|
|
}
|
|
}
|
|
|
|
// Otherwise, check to see if any of the provided actions match.
|
|
for (const action of providedCodeActions) {
|
|
if (!action.kind) {
|
|
continue;
|
|
}
|
|
|
|
for (const entry of documentation) {
|
|
if (entry.kind.contains(new CodeActionKind(action.kind))) {
|
|
return entry.command;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise<ReadonlyArray<modes.CodeAction>> {
|
|
const { resource, rangeOrSelection, kind } = args;
|
|
if (!(resource instanceof URI)) {
|
|
throw illegalArgument();
|
|
}
|
|
|
|
const model = accessor.get(IModelService).getModel(resource);
|
|
if (!model) {
|
|
throw illegalArgument();
|
|
}
|
|
|
|
const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection)
|
|
? Selection.liftSelection(rangeOrSelection)
|
|
: Range.isIRange(rangeOrSelection)
|
|
? model.validateRange(rangeOrSelection)
|
|
: undefined;
|
|
|
|
if (!validatedRangeOrSelection) {
|
|
throw illegalArgument();
|
|
}
|
|
|
|
const codeActionSet = await getCodeActions(
|
|
model,
|
|
validatedRangeOrSelection,
|
|
{ type: modes.CodeActionTriggerType.Manual, filter: { includeSourceActions: true, include: kind && kind.value ? new CodeActionKind(kind.value) : undefined } },
|
|
Progress.None,
|
|
CancellationToken.None);
|
|
|
|
setTimeout(() => codeActionSet.dispose(), 100);
|
|
return codeActionSet.validActions;
|
|
});
|