mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
make feedback explicit and stateful (#178007)
This commit is contained in:
parent
ee036026aa
commit
2f1f07ec2b
|
@ -28,7 +28,7 @@ export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorS
|
|||
this._registrations.dispose();
|
||||
}
|
||||
|
||||
async $registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void> {
|
||||
async $registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise<void> {
|
||||
const unreg = this._interactiveEditorService.addProvider({
|
||||
debugName,
|
||||
prepareInteractiveEditorSession: async (model, range, token) => {
|
||||
|
@ -49,6 +49,9 @@ export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorS
|
|||
result.edits = reviveWorkspaceEditDto(result.edits, this._uriIdentService);
|
||||
}
|
||||
return <IInteractiveEditorResponse | undefined>result;
|
||||
},
|
||||
handleInteractiveEditorResponseFeedback: !supportsFeedback ? undefined : async (session, response, kind) => {
|
||||
this._proxy.$handleFeedback(handle, session.id, response.id, kind);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -195,7 +195,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
|
||||
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
|
||||
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
|
||||
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService, extHostCommands));
|
||||
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService));
|
||||
const extHostInteractiveSession = rpcProtocol.set(ExtHostContext.ExtHostInteractiveSession, new ExtHostInteractiveSession(rpcProtocol, extHostLogService));
|
||||
|
||||
// Check that no named customers are missing
|
||||
|
|
|
@ -26,7 +26,7 @@ import * as languages from 'vs/editor/common/languages';
|
|||
import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration';
|
||||
import { EndOfLineSequence } from 'vs/editor/common/model';
|
||||
import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel';
|
||||
import { IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorSession } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { IInteractiveEditorRequest, IInteractiveEditorResponse, IInteractiveEditorSession, InteractiveEditorResponseFeedbackKind } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
@ -1072,7 +1072,7 @@ export interface MainThreadInteractiveShape extends IDisposable {
|
|||
}
|
||||
|
||||
export interface MainThreadInteractiveEditorShape extends IDisposable {
|
||||
$registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void>;
|
||||
$registerInteractiveEditorProvider(handle: number, debugName: string, supportsFeedback: boolean): Promise<void>;
|
||||
$unregisterInteractiveEditorProvider(handle: number): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -1081,6 +1081,7 @@ export type IInteractiveEditorResponseDto = Dto<IInteractiveEditorResponse>;
|
|||
export interface ExtHostInteractiveEditorShape {
|
||||
$prepareInteractiveSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise<IInteractiveEditorSession | undefined>;
|
||||
$provideResponse(handle: number, session: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): Promise<IInteractiveEditorResponseDto | undefined>;
|
||||
$handleFeedback(handle: number, sessionId: number, responseId: number, kind: InteractiveEditorResponseFeedbackKind): void;
|
||||
$releaseSession(handle: number, sessionId: number): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ISelection } from 'vs/editor/common/core/selection';
|
||||
import { IInteractiveEditorSession, IInteractiveEditorRequest } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { IInteractiveEditorSession, IInteractiveEditorRequest, InteractiveEditorResponseFeedbackKind } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ExtHostInteractiveEditorShape, IInteractiveEditorResponseDto, IMainContext, MainContext, MainThreadInteractiveEditorShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
@ -15,7 +15,6 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
|
|||
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypes';
|
||||
import type * as vscode from 'vscode';
|
||||
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
|
||||
class ProviderWrapper {
|
||||
|
||||
|
@ -31,7 +30,7 @@ class ProviderWrapper {
|
|||
|
||||
class SessionWrapper {
|
||||
|
||||
readonly store = new DisposableStore();
|
||||
readonly responses: (vscode.InteractiveEditorResponse | vscode.InteractiveEditorMessageResponse)[] = [];
|
||||
|
||||
constructor(
|
||||
readonly session: vscode.InteractiveEditorSession
|
||||
|
@ -50,7 +49,6 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
mainContext: IMainContext,
|
||||
private readonly _documents: ExtHostDocuments,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _commands: ExtHostCommands,
|
||||
) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveEditor);
|
||||
}
|
||||
|
@ -58,7 +56,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
registerProvider(extension: Readonly<IRelaxedExtensionDescription>, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable {
|
||||
const wrapper = new ProviderWrapper(extension, provider);
|
||||
this._inputProvider.set(wrapper.handle, wrapper);
|
||||
this._proxy.$registerInteractiveEditorProvider(wrapper.handle, extension.identifier.value);
|
||||
this._proxy.$registerInteractiveEditorProvider(wrapper.handle, extension.identifier.value, typeof provider.handleInteractiveEditorResponseFeedback === 'function');
|
||||
return toDisposable(() => {
|
||||
this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle);
|
||||
this._inputProvider.delete(wrapper.handle);
|
||||
|
@ -113,15 +111,17 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
|
||||
if (res) {
|
||||
|
||||
const id = sessionData.responses.push(res) - 1;
|
||||
|
||||
const stub: Partial<IInteractiveEditorResponseDto> = {
|
||||
wholeRange: typeConvert.Range.from(res.wholeRange),
|
||||
placeholder: res.placeholder,
|
||||
commands: res.commands ? res.commands.map(c => this._commands.converter.toInternal(c, sessionData.store)) : undefined,
|
||||
};
|
||||
|
||||
if (ExtHostInteractiveEditor._isMessageResponse(res)) {
|
||||
return {
|
||||
...stub,
|
||||
id,
|
||||
type: 'message',
|
||||
message: typeConvert.MarkdownString.from(res.contents),
|
||||
};
|
||||
|
@ -131,6 +131,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
if (edits instanceof WorkspaceEdit) {
|
||||
return {
|
||||
...stub,
|
||||
id,
|
||||
type: 'bulkEdit',
|
||||
edits: typeConvert.WorkspaceEdit.from(edits),
|
||||
};
|
||||
|
@ -138,6 +139,7 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
} else if (Array.isArray(edits)) {
|
||||
return {
|
||||
...stub,
|
||||
id,
|
||||
type: 'editorEdit',
|
||||
edits: edits.map(typeConvert.TextEdit.from),
|
||||
};
|
||||
|
@ -147,12 +149,20 @@ export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
$handleFeedback(handle: number, sessionId: number, responseId: number, kind: InteractiveEditorResponseFeedbackKind): void {
|
||||
const entry = this._inputProvider.get(handle);
|
||||
const sessionData = this._inputSessions.get(sessionId);
|
||||
const response = sessionData?.responses[responseId];
|
||||
if (entry && response) {
|
||||
entry.provider.handleInteractiveEditorResponseFeedback?.(sessionData.session, response, kind === InteractiveEditorResponseFeedbackKind.Helpful ? true : false);
|
||||
}
|
||||
}
|
||||
|
||||
$releaseSession(handle: number, sessionId: number) {
|
||||
const sessionData = this._inputSessions.get(sessionId);
|
||||
const entry = this._inputProvider.get(handle);
|
||||
if (sessionData && entry) {
|
||||
entry.provider.releaseInteractiveEditorSession?.(sessionData.session);
|
||||
sessionData.store.dispose();
|
||||
}
|
||||
this._inputSessions.delete(sessionId);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
|
|||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, IInteractiveEditorRequest, IInteractiveEditorSession, IInteractiveEditorSlashCommand } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, IInteractiveEditorRequest, IInteractiveEditorSession, IInteractiveEditorSlashCommand, IInteractiveEditorSessionProvider, InteractiveEditorResponseFeedbackKind } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { ICursorStateComputer, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
|
||||
|
@ -52,10 +52,9 @@ import { IViewsService } from 'vs/workbench/common/views';
|
|||
import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
|
||||
import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionSidebar';
|
||||
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
|
||||
import { Command, CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';
|
||||
import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages';
|
||||
import { LanguageSelector } from 'vs/editor/common/languageSelector';
|
||||
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
|
||||
|
||||
class InteractiveEditorWidget {
|
||||
|
@ -503,14 +502,6 @@ export class InteractiveEditorZoneWidget extends ZoneWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class CommandAction extends Action {
|
||||
|
||||
constructor(command: Command, @ICommandService commandService: ICommandService) {
|
||||
const icon = ThemeIcon.fromString(command.title);
|
||||
super(command.id, icon ? command.tooltip : command.title, icon ? ThemeIcon.asClassName(icon) : undefined, true, () => commandService.executeCommand(command.id, ...(command.arguments ?? [])));
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleInlineDiff extends Action {
|
||||
|
||||
constructor(private readonly _inlineDiff: InlineDiffDecorations) {
|
||||
|
@ -654,6 +645,63 @@ class InlineDiffDecorations {
|
|||
}
|
||||
}
|
||||
|
||||
class FeedbackToggles {
|
||||
|
||||
private readonly _onDidChange = new Emitter<this>();
|
||||
readonly onDidChange: Event<this> = this._onDidChange.event;
|
||||
|
||||
private readonly _helpful: Action;
|
||||
private readonly _unHelpful: Action;
|
||||
|
||||
constructor(provider: IInteractiveEditorSessionProvider, session: IInteractiveEditorSession, response: IInteractiveEditorResponse) {
|
||||
|
||||
const supportsFeedback = typeof provider.handleInteractiveEditorResponseFeedback === 'function';
|
||||
|
||||
const update = (kind: InteractiveEditorResponseFeedbackKind) => {
|
||||
if (supportsFeedback) {
|
||||
provider.handleInteractiveEditorResponseFeedback!(session, response, kind);
|
||||
|
||||
if (kind === InteractiveEditorResponseFeedbackKind.Helpful) {
|
||||
this._helpful.tooltip = localize('thanks', "Thanks for your feedback!");
|
||||
this._helpful.checked = true;
|
||||
this._helpful.enabled = false;
|
||||
this._unHelpful.enabled = false;
|
||||
} else {
|
||||
this._unHelpful.tooltip = localize('thanks', "Thanks for your feedback!");
|
||||
this._unHelpful.checked = true;
|
||||
this._unHelpful.enabled = false;
|
||||
this._helpful.enabled = false;
|
||||
}
|
||||
|
||||
this._onDidChange.fire(this);
|
||||
}
|
||||
};
|
||||
|
||||
this._helpful = new Action('interactiveEditor.helpful', localize('helpful', "Vote Up"), ThemeIcon.asClassName(Codicon.thumbsup), supportsFeedback, () => update(InteractiveEditorResponseFeedbackKind.Helpful));
|
||||
this._unHelpful = new Action('interactiveEditor.unHelpful', localize('unhelpful', "Vote Down"), ThemeIcon.asClassName(Codicon.thumbsdown), supportsFeedback, () => update(InteractiveEditorResponseFeedbackKind.Unhelpful));
|
||||
|
||||
this._helpful.tooltip = this._helpful.label;
|
||||
this._unHelpful.tooltip = this._unHelpful.label;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidChange.dispose();
|
||||
this._helpful.dispose();
|
||||
this._unHelpful.dispose();
|
||||
}
|
||||
|
||||
get actions() {
|
||||
const result: IAction[] = [];
|
||||
if (this._helpful.enabled || this._helpful.checked) {
|
||||
result.push(this._helpful);
|
||||
}
|
||||
if (this._unHelpful.enabled || this._unHelpful.checked) {
|
||||
result.push(this._unHelpful);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class InteractiveEditorController implements IEditorContribution {
|
||||
|
||||
static ID = 'interactiveEditor';
|
||||
|
@ -962,17 +1010,19 @@ export class InteractiveEditorController implements IEditorContribution {
|
|||
|
||||
inlineDiffDecorations.update();
|
||||
|
||||
|
||||
const replyActions: Action[] = reply.commands?.map(command => this._instaService.createInstance(CommandAction, command)) ?? [];
|
||||
const fixedActions: Action[] = [new UndoAction(textModel), new ToggleInlineDiff(inlineDiffDecorations)];
|
||||
roundStore.add(combinedDisposable(...replyActions, ...fixedActions));
|
||||
roundStore.add(combinedDisposable(...fixedActions));
|
||||
|
||||
const feedback = new FeedbackToggles(provider, session, reply);
|
||||
roundStore.add(feedback);
|
||||
roundStore.add(feedback.onDidChange(() => { statusWidget.update({ actions: Separator.join(feedback.actions, fixedActions) }); }));
|
||||
|
||||
const editsCount = (moreMinimalEdits ?? reply.edits).length;
|
||||
|
||||
statusWidget.update({
|
||||
message: editsCount === 1 ? localize('edit.1', "Done, made 1 change") : localize('edit.N', "Done, made {0} changes", editsCount),
|
||||
classes: [],
|
||||
actions: Separator.join(replyActions, fixedActions),
|
||||
actions: Separator.join(feedback.actions, fixedActions),
|
||||
});
|
||||
|
||||
if (!InteractiveEditorController._promptHistory.includes(input.value)) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
|
|||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { ISelection } from 'vs/editor/common/core/selection';
|
||||
import { Command, ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
|
||||
import { ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { localize } from 'vs/nls';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
|
@ -39,27 +39,32 @@ export interface IInteractiveEditorRequest {
|
|||
export type IInteractiveEditorResponse = IInteractiveEditorEditResponse | IInteractiveEditorBulkEditResponse | IInteractiveEditorMessageResponse;
|
||||
|
||||
export interface IInteractiveEditorEditResponse {
|
||||
id: number;
|
||||
type: 'editorEdit';
|
||||
edits: TextEdit[];
|
||||
placeholder?: string;
|
||||
wholeRange?: IRange;
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
export interface IInteractiveEditorBulkEditResponse {
|
||||
id: number;
|
||||
type: 'bulkEdit';
|
||||
edits: WorkspaceEdit;
|
||||
placeholder?: string;
|
||||
wholeRange?: IRange;
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
export interface IInteractiveEditorMessageResponse {
|
||||
id: number;
|
||||
type: 'message';
|
||||
message: IMarkdownString;
|
||||
placeholder?: string;
|
||||
wholeRange?: IRange;
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
export const enum InteractiveEditorResponseFeedbackKind {
|
||||
Helpful,
|
||||
Unhelpful
|
||||
}
|
||||
|
||||
export interface IInteractiveEditorSessionProvider {
|
||||
|
@ -69,6 +74,8 @@ export interface IInteractiveEditorSessionProvider {
|
|||
prepareInteractiveEditorSession(model: ITextModel, range: ISelection, token: CancellationToken): ProviderResult<IInteractiveEditorSession>;
|
||||
|
||||
provideResponse(item: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): ProviderResult<IInteractiveEditorResponse>;
|
||||
|
||||
handleInteractiveEditorResponseFeedback?(session: IInteractiveEditorSession, response: IInteractiveEditorResponse, kind: InteractiveEditorResponseFeedbackKind): void;
|
||||
}
|
||||
|
||||
export const IInteractiveEditorService = createDecorator<IInteractiveEditorService>('IInteractiveEditorService');
|
||||
|
|
|
@ -33,7 +33,6 @@ declare module 'vscode' {
|
|||
edits: TextEdit[] | WorkspaceEdit;
|
||||
placeholder?: string;
|
||||
wholeRange?: Range;
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
// todo@API make classes
|
||||
|
@ -41,7 +40,6 @@ declare module 'vscode' {
|
|||
contents: MarkdownString;
|
||||
placeholder?: string;
|
||||
wholeRange?: Range;
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
export interface TextDocumentContext {
|
||||
|
@ -58,6 +56,10 @@ declare module 'vscode' {
|
|||
|
||||
// eslint-disable-next-line local/vscode-dts-provider-naming
|
||||
releaseInteractiveEditorSession?(session: InteractiveEditorSession): any;
|
||||
|
||||
// todo@API use enum instead of boolean
|
||||
// eslint-disable-next-line local/vscode-dts-provider-naming
|
||||
handleInteractiveEditorResponseFeedback?(session: InteractiveEditorSession, response: InteractiveEditorResponse | InteractiveEditorMessageResponse, helpful: boolean): void;
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue