Replacing heapservice with lifecycle for code actions

Part of #74846

Code Actions can use commands internally, which must be disposed of. We were previously using the heap service for this but this will not work for the web. Add a custom lifecycle instead
This commit is contained in:
Matt Bierner
2019-06-06 22:11:11 -07:00
parent 0aaf00bb6a
commit b4a00ca33f
14 changed files with 189 additions and 110 deletions

View File

@@ -11,7 +11,7 @@ import * as search from 'vs/workbench/contrib/search/common/search';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Position as EditorPosition } from 'vs/editor/common/core/position';
import { Range as EditorRange, IRange } from 'vs/editor/common/core/range';
import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CallHierarchyDto, SuggestDataDto } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CallHierarchyDto, SuggestDataDto, CodeActionDto } from '../common/extHost.protocol';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration';
import { IModeService } from 'vs/editor/common/services/modeService';
@@ -20,24 +20,20 @@ import { URI } from 'vs/base/common/uri';
import { Selection } from 'vs/editor/common/core/selection';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { IHeapService } from 'vs/workbench/services/heap/common/heap';
import { mixin } from 'vs/base/common/objects';
@extHostNamedCustomer(MainContext.MainThreadLanguageFeatures)
export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape {
private readonly _proxy: ExtHostLanguageFeaturesShape;
private readonly _heapService: IHeapService;
private readonly _modeService: IModeService;
private readonly _registrations: { [handle: number]: IDisposable; } = Object.create(null);
constructor(
extHostContext: IExtHostContext,
@IHeapService heapService: IHeapService,
@IModeService modeService: IModeService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures);
this._heapService = heapService;
this._modeService = modeService;
}
@@ -100,7 +96,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
}
}
private static _reviveCodeActionDto(data: CodeActionDto[] | undefined): modes.CodeAction[] {
private static _reviveCodeActionDto(data: ReadonlyArray<CodeActionDto>): modes.CodeAction[] {
if (data) {
data.forEach(code => reviveWorkspaceEditDto(code.edit));
}
@@ -239,13 +235,19 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
$registerQuickFixSupport(handle: number, selector: ISerializedDocumentFilter[], providedCodeActionKinds?: string[]): void {
this._registrations[handle] = modes.CodeActionProviderRegistry.register(selector, <modes.CodeActionProvider>{
provideCodeActions: (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise<modes.CodeAction[]> => {
return this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token).then(dto => {
if (dto) {
dto.forEach(obj => { this._heapService.trackObject(obj.command); });
provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise<modes.CodeActionList | undefined> => {
const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token);
if (!listDto) {
return undefined;
}
return <modes.CodeActionList>{
actions: MainThreadLanguageFeatures._reviveCodeActionDto(listDto.actions),
dispose: () => {
if (typeof listDto.cacheId === 'number') {
this._proxy.$releaseCodeActions(handle, listDto.cacheId);
}
}
return MainThreadLanguageFeatures._reviveCodeActionDto(dto);
});
};
},
providedCodeActionKinds
});

View File

@@ -305,6 +305,8 @@ class CodeActionOnSaveParticipant implements ISaveParticipant {
await this.applyCodeActions(actionsToRun.actions);
} catch {
// Failure to apply a code action should not block other on save actions
} finally {
actionsToRun.dispose();
}
}
}

View File

@@ -993,6 +993,11 @@ export interface CodeActionDto {
isPreferred?: boolean;
}
export interface CodeActionListDto {
cacheId: number;
actions: ReadonlyArray<CodeActionDto>;
}
export type CacheId = number;
export type ChainedCacheId = [CacheId, CacheId];
@@ -1041,7 +1046,8 @@ export interface ExtHostLanguageFeaturesShape {
$provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.Hover | undefined>;
$provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.DocumentHighlight[] | undefined>;
$provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise<LocationDto[] | undefined>;
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionDto[] | undefined>;
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionListDto | undefined>;
$releaseCodeActions(handle: number, cacheId: number): void;
$provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;

View File

@@ -15,7 +15,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics';
import { asPromise } from 'vs/base/common/async';
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, SuggestDataDto, LinksListDto, ChainedCacheId, CodeLensListDto } from './extHost.protocol';
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, SuggestDataDto, LinksListDto, ChainedCacheId, CodeLensListDto, CodeActionListDto } from './extHost.protocol';
import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings';
import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range as EditorRange } from 'vs/editor/common/core/range';
@@ -307,6 +307,9 @@ export interface CustomCodeAction extends CodeActionDto {
class CodeActionAdapter {
private static readonly _maxCodeActionsPerFile: number = 1000;
private readonly _cache = new Cache<vscode.CodeAction | vscode.Command>();
private readonly _disposables = new Map<number, DisposableStore>();
constructor(
private readonly _documents: ExtHostDocuments,
private readonly _commands: CommandsConverter,
@@ -316,7 +319,7 @@ class CodeActionAdapter {
private readonly _extensionId: ExtensionIdentifier
) { }
provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionDto[] | undefined> {
provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionListDto | undefined> {
const doc = this._documents.getDocument(resource);
const ran = Selection.isISelection(rangeOrSelection)
@@ -339,34 +342,39 @@ class CodeActionAdapter {
};
return asPromise(() => this._provider.provideCodeActions(doc, ran, codeActionContext, token)).then(commandsOrActions => {
if (!isNonEmptyArray(commandsOrActions)) {
if (!isNonEmptyArray(commandsOrActions) || token.isCancellationRequested) {
return undefined;
}
const result: CustomCodeAction[] = [];
const cacheId = this._cache.add(commandsOrActions);
const disposables = new DisposableStore();
this._disposables.set(cacheId, disposables);
const actions: CustomCodeAction[] = [];
for (const candidate of commandsOrActions) {
if (!candidate) {
continue;
}
if (CodeActionAdapter._isCommand(candidate)) {
// old school: synthetic code action
result.push({
actions.push({
_isSynthetic: true,
title: candidate.title,
command: this._commands.toInternal(candidate),
command: this._commands.toInternal2(candidate, disposables),
});
} else {
if (codeActionContext.only) {
if (!candidate.kind) {
this._logService.warn(`${this._extensionId.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`);
} else if (!codeActionContext.only.contains(candidate.kind)) {
this._logService.warn(`${this._extensionId.value} -Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`);
this._logService.warn(`${this._extensionId.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`);
}
}
// new school: convert code action
result.push({
actions.push({
title: candidate.title,
command: candidate.command && this._commands.toInternal(candidate.command),
command: candidate.command && this._commands.toInternal2(candidate.command, disposables),
diagnostics: candidate.diagnostics && candidate.diagnostics.map(typeConvert.Diagnostic.from),
edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit),
kind: candidate.kind && candidate.kind.value,
@@ -375,10 +383,16 @@ class CodeActionAdapter {
}
}
return result;
return <CodeActionListDto>{ cacheId, actions };
});
}
public releaseCodeActions(cachedId: number): void {
dispose(this._disposables.get(cachedId));
this._disposables.delete(cachedId);
this._cache.delete(cachedId);
}
private static _isCommand(thing: any): thing is vscode.Command {
return typeof (<vscode.Command>thing).command === 'string' && typeof (<vscode.Command>thing).title === 'string';
}
@@ -1276,10 +1290,14 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
}
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionDto[] | undefined> {
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<CodeActionListDto | undefined> {
return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), rangeOrSelection, context, token), undefined);
}
$releaseCodeActions(handle: number, cacheId: number): void {
this._withAdapter(handle, CodeActionAdapter, adapter => Promise.resolve(adapter.releaseCodeActions(cacheId)), undefined);
}
// --- formatting
registerDocumentFormattingEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentFormattingEditProvider): vscode.Disposable {