Path: blob/main/extensions/copilot/test/simulation/fixtures/gen/inlayHintsController.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ModifierKeyEmitter } from 'vs/base/browser/dom';6import { isNonEmptyArray } from 'vs/base/common/arrays';7import { RunOnceScheduler } from 'vs/base/common/async';8import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';9import { onUnexpectedError } from 'vs/base/common/errors';10import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';11import { LRUCache } from 'vs/base/common/map';12import { IRange } from 'vs/base/common/range';13import { assertType } from 'vs/base/common/types';14import { URI } from 'vs/base/common/uri';15import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';16import { ClassNameReference, CssProperties, DynamicCssRules } from 'vs/editor/browser/editorDom';17import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';18import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';19import { EditOperation } from 'vs/editor/common/core/editOperation';20import { Range } from 'vs/editor/common/core/range';21import { IEditorContribution } from 'vs/editor/common/editorCommon';22import * as languages from 'vs/editor/common/languages';23import { IModelDeltaDecoration, InjectedTextCursorStops, InjectedTextOptions, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';24import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel';25import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';26import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';27import { ITextModelService } from 'vs/editor/common/services/resolverService';28import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';29import { InlayHintAnchor, InlayHintItem, InlayHintsFragments } from 'vs/editor/contrib/inlayHints/browser/inlayHints';30import { goToDefinitionWithLocation, showGoToContextMenu } from 'vs/editor/contrib/inlayHints/browser/inlayHintsLocations';31import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';32import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';33import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';34import { INotificationService, Severity } from 'vs/platform/notification/common/notification';35import * as colors from 'vs/platform/theme/common/colorRegistry';36import { themeColorFromId } from 'vs/platform/theme/common/themeService';3738// --- hint caching service (per session)3940class InlayHintsCache {4142declare readonly _serviceBrand: undefined;4344private readonly _entries = new LRUCache<string, InlayHintItem[]>(50);4546get(model: ITextModel): InlayHintItem[] | undefined {47const key = InlayHintsCache._key(model);48return this._entries.get(key);49}5051set(model: ITextModel, value: InlayHintItem[]): void {52const key = InlayHintsCache._key(model);53this._entries.set(key, value);54}5556private static _key(model: ITextModel): string {57return `${model.uri.toString()}/${model.getVersionId()}`;58}59}6061interface IInlayHintsCache extends InlayHintsCache { }62const IInlayHintsCache = createDecorator<IInlayHintsCache>('IInlayHintsCache');63registerSingleton(IInlayHintsCache, InlayHintsCache, InstantiationType.Delayed);6465// --- rendered label6667export class RenderedInlayHintLabelPart {68constructor(readonly item: InlayHintItem, readonly index: number) { }6970get part() {71const label = this.item.hint.label;72if (typeof label === 'string') {73return { label };74} else {75return label[this.index];76}77}78}7980class ActiveInlayHintInfo {81constructor(readonly part: RenderedInlayHintLabelPart, readonly hasTriggerModifier: boolean) { }82}8384type InlayHintDecorationRenderInfo = {85item: InlayHintItem;86decoration: IModelDeltaDecoration;87classNameRef: ClassNameReference;88};8990const enum RenderMode {91Normal,92Invisible93}9495// --- controller9697export class InlayHintsController implements IEditorContribution {9899static readonly ID: string = 'editor.contrib.InlayHints';100101private static readonly _MAX_DECORATORS = 1500;102private static readonly _MAX_LABEL_LEN = 43;103104static get(editor: ICodeEditor): InlayHintsController | undefined {105return editor.getContribution<InlayHintsController>(InlayHintsController.ID) ?? undefined;106}107108private readonly _disposables = new DisposableStore();109private readonly _sessionDisposables = new DisposableStore();110private readonly _debounceInfo: IFeatureDebounceInformation;111private readonly _decorationsMetadata = new Map<string, InlayHintDecorationRenderInfo>();112private readonly _ruleFactory = new DynamicCssRules(this._editor);113114private _activeRenderMode = RenderMode.Normal;115private _activeInlayHintPart?: ActiveInlayHintInfo;116117constructor(118private readonly _editor: ICodeEditor,119@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,120@ILanguageFeatureDebounceService _featureDebounce: ILanguageFeatureDebounceService,121@IInlayHintsCache private readonly _inlayHintsCache: IInlayHintsCache,122@ICommandService private readonly _commandService: ICommandService,123@INotificationService private readonly _notificationService: INotificationService,124@IInstantiationService private readonly _instaService: IInstantiationService,125) {126this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 });127this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update()));128this._disposables.add(_editor.onDidChangeModel(() => this._update()));129this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update()));130this._disposables.add(_editor.onDidChangeConfiguration(e => {131if (e.hasChanged(EditorOption.inlayHints)) {132this._update();133}134}));135this._update();136137}138139dispose(): void {140this._sessionDisposables.dispose();141this._removeAllDecorations();142this._disposables.dispose();143}144145private _update(): void {146this._sessionDisposables.clear();147this._removeAllDecorations();148149const options = this._editor.getOption(EditorOption.inlayHints);150if (options.enabled === 'off') {151return;152}153154const model = this._editor.getModel();155if (!model || !this._languageFeaturesService.inlayHintsProvider.has(model)) {156return;157}158159if (options.enabled === 'on') {160// different "on" modes: always161this._activeRenderMode = RenderMode.Normal;162} else {163// different "on" modes: offUnlessPressed, or onUnlessPressed164let defaultMode: RenderMode;165let altMode: RenderMode;166if (options.enabled === 'onUnlessPressed') {167defaultMode = RenderMode.Normal;168altMode = RenderMode.Invisible;169} else {170defaultMode = RenderMode.Invisible;171altMode = RenderMode.Normal;172}173this._activeRenderMode = defaultMode;174175this._sessionDisposables.add(ModifierKeyEmitter.getInstance().event(e => {176if (!this._editor.hasModel()) {177return;178}179const newRenderMode = e.altKey && e.ctrlKey && !(e.shiftKey || e.metaKey) ? altMode : defaultMode;180if (newRenderMode !== this._activeRenderMode) {181this._activeRenderMode = newRenderMode;182const model = this._editor.getModel();183const copies = this._copyInlayHintsWithCurrentAnchor(model);184this._updateHintsDecorators([model.getFullModelRange()], copies);185scheduler.schedule(0);186}187}));188}189190// iff possible, quickly update from cache191const cached = this._inlayHintsCache.get(model);192if (cached) {193this._updateHintsDecorators([model.getFullModelRange()], cached);194}195this._sessionDisposables.add(toDisposable(() => {196// cache items when switching files etc197if (!model.isDisposed()) {198this._cacheHintsForFastRestore(model);199}200}));201202let cts: CancellationTokenSource | undefined;203const watchedProviders = new Set<languages.InlayHintsProvider>();204205const scheduler = new RunOnceScheduler(async () => {206const t1 = Date.now();207208cts?.dispose(true);209cts = new CancellationTokenSource();210const listener = model.onWillDispose(() => cts?.cancel());211212try {213const myToken = cts.token;214const inlayHints = await InlayHintsFragments.create(this._languageFeaturesService.inlayHintsProvider, model, this._getHintsRanges(), myToken);215scheduler.delay = this._debounceInfo.update(model, Date.now() - t1);216if (myToken.isCancellationRequested) {217inlayHints.dispose();218return;219}220221// listen to provider changes222for (const provider of inlayHints.provider) {223if (typeof provider.onDidChangeInlayHints === 'function' && !watchedProviders.has(provider)) {224watchedProviders.add(provider);225this._sessionDisposables.add(provider.onDidChangeInlayHints(() => {226if (!scheduler.isScheduled()) { // ignore event when request is already scheduled227scheduler.schedule();228}229}));230}231}232233this._sessionDisposables.add(inlayHints);234this._updateHintsDecorators(inlayHints.ranges, inlayHints.items);235this._cacheHintsForFastRestore(model);236237} catch (err) {238onUnexpectedError(err);239240} finally {241cts.dispose();242listener.dispose();243}244245}, this._debounceInfo.get(model));246247this._sessionDisposables.add(scheduler);248this._sessionDisposables.add(toDisposable(() => cts?.dispose(true)));249scheduler.schedule(0);250251this._sessionDisposables.add(this._editor.onDidScrollChange((e) => {252// update when scroll position changes253// uses scrollTopChanged has weak heuristic to differenatiate between scrolling due to254// typing or due to "actual" scrolling255if (e.scrollTopChanged || !scheduler.isScheduled()) {256scheduler.schedule();257}258}));259this._sessionDisposables.add(this._editor.onDidChangeModelContent((e) => {260cts?.cancel();261262// update less aggressive when typing263const delay = Math.max(scheduler.delay, 1250);264scheduler.schedule(delay);265}));266267// mouse gestures268this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0)));269this._sessionDisposables.add(this._installLinkGesture());270this._sessionDisposables.add(this._installContextMenu());271}272273private _installLinkGesture(): IDisposable {274275const store = new DisposableStore();276const gesture = store.add(new ClickLinkGesture(this._editor));277278// let removeHighlight = () => { };279280const sessionStore = new DisposableStore();281store.add(sessionStore);282283store.add(gesture.onMouseMoveOrRelevantKeyDown(e => {284const [mouseEvent] = e;285const labelPart = this._getInlayHintLabelPart(mouseEvent);286const model = this._editor.getModel();287288if (!labelPart || !model) {289sessionStore.clear();290return;291}292293// resolve the item294const cts = new CancellationTokenSource();295sessionStore.add(toDisposable(() => cts.dispose(true)));296labelPart.item.resolve(cts.token);297298// render link => when the modifier is pressed and when there is a command or location299this._activeInlayHintPart = labelPart.part.command || labelPart.part.location300? new ActiveInlayHintInfo(labelPart, mouseEvent.hasTriggerModifier)301: undefined;302303const lineNumber = model.validatePosition(labelPart.item.hint.position).lineNumber;304const range = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));305const lineHints = this._getInlineHintsForRange(range);306this._updateHintsDecorators([range], lineHints);307sessionStore.add(toDisposable(() => {308this._activeInlayHintPart = undefined;309this._updateHintsDecorators([range], lineHints);310}));311}));312store.add(gesture.onCancel(() => sessionStore.clear()));313store.add(gesture.onExecute(async e => {314const label = this._getInlayHintLabelPart(e);315if (label) {316const part = label.part;317if (part.location) {318// location -> execute go to def319this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, part.location);320} else if (languages.Command.is(part.command)) {321// command -> execute it322await this._invokeCommand(part.command, label.item);323}324}325}));326return store;327}328329private _getInlineHintsForRange(range: Range) {330const lineHints = new Set<InlayHintItem>();331for (const data of this._decorationsMetadata.values()) {332if (range.containsRange(data.item.anchor.range)) {333lineHints.add(data.item);334}335}336return Array.from(lineHints);337}338339private _installDblClickGesture(updateInlayHints: Function): IDisposable {340return this._editor.onMouseUp(async e => {341if (e.event.detail !== 2) {342return;343}344const part = this._getInlayHintLabelPart(e);345if (!part) {346return;347}348e.event.preventDefault();349await part.item.resolve(CancellationToken.None);350if (isNonEmptyArray(part.item.hint.textEdits)) {351const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));352this._editor.executeEdits('inlayHint.default', edits);353updateInlayHints();354}355});356}357358private _installContextMenu(): IDisposable {359return this._editor.onContextMenu(async e => {360if (!(e.event.target instanceof HTMLElement)) {361return;362}363const part = this._getInlayHintLabelPart(e);364if (part) {365await this._instaService.invokeFunction(showGoToContextMenu, this._editor, e.event.target, part);366}367});368}369370private _getInlayHintLabelPart(e: IEditorMouseEvent | ClickLinkMouseEvent): RenderedInlayHintLabelPart | undefined {371if (e.target.type !== MouseTargetType.CONTENT_TEXT) {372return undefined;373}374const options = e.target.detail.injectedText?.options;375if (options instanceof ModelDecorationInjectedTextOptions && options?.attachedData instanceof RenderedInlayHintLabelPart) {376return options.attachedData;377}378return undefined;379}380381private async _invokeCommand(command: languages.Command, item: InlayHintItem) {382try {383await this._commandService.executeCommand(command.id, ...(command.arguments ?? []));384} catch (err) {385this._notificationService.notify({386severity: Severity.Error,387source: item.provider.displayName,388message: err389});390}391}392393private _cacheHintsForFastRestore(model: ITextModel): void {394const hints = this._copyInlayHintsWithCurrentAnchor(model);395this._inlayHintsCache.set(model, hints);396}397398// return inlay hints but with an anchor that reflects "updates"399// that happened after receiving them, e.g adding new lines before a hint400private _copyInlayHintsWithCurrentAnchor(model: ITextModel): InlayHintItem[] {401const items = new Map<InlayHintItem, InlayHintItem>();402for (const [id, obj] of this._decorationsMetadata) {403if (items.has(obj.item)) {404// an inlay item can be rendered as multiple decorations405// but they will all uses the same range406continue;407}408const range = model.getDecorationRange(id);409if (range) {410// update range with whatever the editor has tweaked it to411const anchor = new InlayHintAnchor(range, obj.item.anchor.direction);412const copy = obj.item.with({ anchor });413items.set(obj.item, copy);414}415}416return Array.from(items.values());417}418419private _getHintsRanges(): Range[] {420const extra = 30;421const model = this._editor.getModel()!;422const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow();423const result: Range[] = [];424for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) {425const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn));426if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) {427result.push(extendedRange);428} else {429result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange);430}431}432return result;433}434435private _updateHintsDecorators(ranges: readonly Range[], items: readonly InlayHintItem[]): void {436437// utils to collect/create injected text decorations438const newDecorationsData: InlayHintDecorationRenderInfo[] = [];439const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart): void => {440const opts: InjectedTextOptions = {441content,442inlineClassNameAffectsLetterSpacing: true,443inlineClassName: ref.className,444cursorStops,445attachedData446};447newDecorationsData.push({448item,449classNameRef: ref,450decoration: {451range: item.anchor.range,452options: {453// className: "rangeHighlight", // DEBUG highlight to see to what range a hint is attached454description: 'InlayHint',455showIfCollapsed: item.anchor.range.isEmpty(), // "original" range is empty456collapseOnReplaceEdit: !item.anchor.range.isEmpty(),457stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,458[item.anchor.direction]: this._activeRenderMode === RenderMode.Normal ? opts : undefined459}460}461});462};463464const addInjectedWhitespace = (item: InlayHintItem, isLast: boolean): void => {465const marginRule = this._ruleFactory.createClassNameRef({466width: `${(fontSize / 3) | 0}px`,467display: 'inline-block'468});469addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None);470};471472473//474const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo();475const fontFamilyVar = '--code-editorInlayHintsFontFamily';476this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily);477478479type ILineInfo = { line: number; totalLen: number };480let currentLineInfo: ILineInfo = { line: 0, totalLen: 0 };481482for (const item of items) {483484if (currentLineInfo.line !== item.anchor.range.startLineNumber) {485currentLineInfo = { line: item.anchor.range.startLineNumber, totalLen: 0 };486}487488if (currentLineInfo.totalLen > InlayHintsController._MAX_LABEL_LEN) {489continue;490}491492// whitespace leading the actual label493if (item.hint.paddingLeft) {494addInjectedWhitespace(item, false);495}496497// the label with its parts498const parts: languages.InlayHintLabelPart[] = typeof item.hint.label === 'string'499? [{ label: item.hint.label }]500: item.hint.label;501502for (let i = 0; i < parts.length; i++) {503const part = parts[i];504505const isFirst = i === 0;506const isLast = i === parts.length - 1;507508const cssProperties: CssProperties = {509fontSize: `${fontSize}px`,510fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`,511verticalAlign: isUniform ? 'baseline' : 'middle',512unicodeBidi: 'isolate'513};514515if (isNonEmptyArray(item.hint.textEdits)) {516cssProperties.cursor = 'default';517}518519this._fillInColors(cssProperties, item.hint);520521if ((part.command || part.location) && this._activeInlayHintPart?.part.item === item && this._activeInlayHintPart.part.index === i) {522// active link!523cssProperties.textDecoration = 'underline';524if (this._activeInlayHintPart.hasTriggerModifier) {525cssProperties.color = themeColorFromId(colors.editorActiveLinkForeground);526cssProperties.cursor = 'pointer';527}528}529530if (padding) {531if (isFirst && isLast) {532// only element533cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`;534cssProperties.borderRadius = `${(fontSize / 4) | 0}px`;535} else if (isFirst) {536// first element537cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`;538cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`;539} else if (isLast) {540// last element541cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`;542cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`;543} else {544cssProperties.padding = `1px 0 1px 0`;545}546}547548let textlabel = part.label;549currentLineInfo.totalLen += textlabel.length;550let tooLong = false;551const over = currentLineInfo.totalLen - InlayHintsController._MAX_LABEL_LEN;552if (over > 0) {553textlabel = textlabel.slice(0, -over) + '…';554tooLong = true;555}556557addInjectedText(558item,559this._ruleFactory.createClassNameRef(cssProperties),560fixSpace(textlabel),561isLast && !item.hint.paddingRight ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None,562new RenderedInlayHintLabelPart(item, i)563);564565if (tooLong) {566break;567}568}569570// whitespace trailing the actual label571if (item.hint.paddingRight) {572addInjectedWhitespace(item, true);573}574575if (newDecorationsData.length > InlayHintsController._MAX_DECORATORS) {576break;577}578}579580// collect all decoration ids that are affected by the ranges581// and only update those decorations582const decorationIdsToReplace: string[] = [];583for (const [id, metadata] of this._decorationsMetadata) {584const range = this._editor.getModel()?.getDecorationRange(id);585if (range && ranges.some(r => r.containsRange(range))) {586decorationIdsToReplace.push(id);587metadata.classNameRef.dispose();588this._decorationsMetadata.delete(id);589}590}591592const scrollState = StableEditorScrollState.capture(this._editor);593594this._editor.changeDecorations(accessor => {595const newDecorationIds = accessor.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration));596for (let i = 0; i < newDecorationIds.length; i++) {597const data = newDecorationsData[i];598this._decorationsMetadata.set(newDecorationIds[i], data);599}600});601602scrollState.restore(this._editor);603}604605private _fillInColors(props: CssProperties, hint: languages.InlayHint): void {606if (hint.kind === languages.InlayHintKind.Parameter) {607props.backgroundColor = themeColorFromId(colors.editorInlayHintParameterBackground);608props.color = themeColorFromId(colors.editorInlayHintParameterForeground);609} else if (hint.kind === languages.InlayHintKind.Type) {610props.backgroundColor = themeColorFromId(colors.editorInlayHintTypeBackground);611props.color = themeColorFromId(colors.editorInlayHintTypeForeground);612} else {613props.backgroundColor = themeColorFromId(colors.editorInlayHintBackground);614props.color = themeColorFromId(colors.editorInlayHintForeground);615}616}617618private _getLayoutInfo() {619const options = this._editor.getOption(EditorOption.inlayHints);620const padding = options.padding;621622const editorFontSize = this._editor.getOption(EditorOption.fontSize);623const editorFontFamily = this._editor.getOption(EditorOption.fontFamily);624625let fontSize = options.fontSize;626if (!fontSize || fontSize < 5 || fontSize > editorFontSize) {627fontSize = editorFontSize;628}629630const fontFamily = options.fontFamily || editorFontFamily;631632const isUniform = !padding633&& fontFamily === editorFontFamily634&& fontSize === editorFontSize;635636return { fontSize, fontFamily, padding, isUniform };637}638639private _removeAllDecorations(): void {640this._editor.removeDecorations(Array.from(this._decorationsMetadata.keys()));641for (const obj of this._decorationsMetadata.values()) {642obj.classNameRef.dispose();643}644this._decorationsMetadata.clear();645}646647648// --- accessibility649650getInlayHintsForLine(line: number): InlayHintItem[] {651if (!this._editor.hasModel()) {652return [];653}654const set = new Set<languages.InlayHint>();655const result: InlayHintItem[] = [];656for (const deco of this._editor.getLineDecorations(line)) {657const data = this._decorationsMetadata.get(deco.id);658if (data && !set.has(data.item.hint)) {659set.add(data.item.hint);660result.push(data.item);661}662}663return result;664}665}666667668// Prevents the view from potentially visible whitespace669function fixSpace(str: string): string {670const noBreakWhitespace = '\xa0';671return str.replace(/[ \t]/g, noBreakWhitespace);672}673674CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise<languages.InlayHint[]> => {675676const [uri, range] = args;677assertType(URI.isUri(uri));678assertType(Range.isIRange(range));679680const { inlayHintsProvider } = accessor.get(ILanguageFeaturesService);681const ref = await accessor.get(ITextModelService).createModelReference(uri);682try {683const model = await InlayHintsFragments.create(inlayHintsProvider, ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None);684const result = model.items.map(i => i.hint);685setTimeout(() => model.dispose(), 0); // dispose after sending to ext host686return result;687} finally {688ref.dispose();689}690});691692693