Path: blob/main/extensions/copilot/test/simulation/fixtures/generate/issue-6788/terminalSuggestAddon.ts
13405 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 type { ITerminalAddon, Terminal } from '@xterm/xterm';6import * as dom from 'vs/base/browser/dom';7import { Codicon } from 'vs/base/common/codicons';8import { Emitter, Event } from 'vs/base/common/event';9import { combinedDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle';10import { ThemeIcon } from 'vs/base/common/themables';11import { editorSuggestWidgetSelectedBackground } from 'vs/editor/contrib/suggest/browser/suggestWidget';12import { IContextKey } from 'vs/platform/contextkey/common/contextkey';13import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';14import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';15import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';16import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';17import { SimpleCompletionItem } from 'vs/workbench/services/suggest/browser/simpleCompletionItem';18import { LineContext, SimpleCompletionModel } from 'vs/workbench/services/suggest/browser/simpleCompletionModel';19import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget';2021import { IConfigurationService } from 'vs/platform/configuration/common/configuration';22import { TerminalCapability, type ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';23import type { IPromptInputModel, IPromptInputModelState } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel';24import { ShellIntegrationOscPs } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';25import { getListStyles } from 'vs/platform/theme/browser/defaultStyles';26import type { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';27import { terminalSuggestConfigSection, type ITerminalSuggestConfiguration } from 'vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration';2829export const enum VSCodeSuggestOscPt {30Completions = 'Completions',31CompletionsPwshCommands = 'CompletionsPwshCommands',32CompletionsBash = 'CompletionsBash',33CompletionsBashFirstWord = 'CompletionsBashFirstWord'34}3536export type CompressedPwshCompletion = [37completionText: string,38resultType: number,39toolTip: string40];4142export type PwshCompletion = {43CompletionText: string;44ResultType: number;45ToolTip: string;46};474849/**50* A map of the pwsh result type enum's value to the corresponding icon to use in completions.51*52* | Value | Name | Description53* |-------|-------------------|------------54* | 0 | Text | An unknown result type, kept as text only55* | 1 | History | A history result type like the items out of get-history56* | 2 | Command | A command result type like the items out of get-command57* | 3 | ProviderItem | A provider item58* | 4 | ProviderContainer | A provider container59* | 5 | Property | A property result type like the property items out of get-member60* | 6 | Method | A method result type like the method items out of get-member61* | 7 | ParameterName | A parameter name result type like the Parameters property out of get-command items62* | 8 | ParameterValue | A parameter value result type63* | 9 | Variable | A variable result type like the items out of get-childitem variable:64* | 10 | Namespace | A namespace65* | 11 | Type | A type name66* | 12 | Keyword | A keyword67* | 13 | DynamicKeyword | A dynamic keyword68*69* @see https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.completionresulttype?view=powershellsdk-7.0.070*/71const pwshTypeToIconMap: { [type: string]: ThemeIcon | undefined } = {720: Codicon.symbolText,731: Codicon.history,742: Codicon.symbolMethod,753: Codicon.symbolFile,764: Codicon.folder,775: Codicon.symbolProperty,786: Codicon.symbolMethod,797: Codicon.symbolVariable,808: Codicon.symbolValue,819: Codicon.symbolVariable,8210: Codicon.symbolNamespace,8311: Codicon.symbolInterface,8412: Codicon.symbolKeyword,8513: Codicon.symbolKeyword86};8788export interface ISuggestController {89selectPreviousSuggestion(): void;90selectPreviousPageSuggestion(): void;91selectNextSuggestion(): void;92selectNextPageSuggestion(): void;93acceptSelectedSuggestion(suggestion?: Pick<ISimpleSelectedSuggestion, 'item' | 'model'>): void;94hideSuggestWidget(): void;95}9697export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggestController {98private _terminal?: Terminal;99100private _promptInputModel?: IPromptInputModel;101private readonly _promptInputModelSubscriptions = this._register(new MutableDisposable());102103private _mostRecentPromptInputState?: IPromptInputModelState;104private _initialPromptInputState?: IPromptInputModelState;105private _currentPromptInputState?: IPromptInputModelState;106private _model?: SimpleCompletionModel;107108private _panel?: HTMLElement;109private _screen?: HTMLElement;110private _suggestWidget?: SimpleSuggestWidget;111private _enableWidget: boolean = true;112113// TODO: Remove these in favor of prompt input state114private _leadingLineContent?: string;115private _cursorIndexDelta: number = 0;116117private _lastUserDataTimestamp: number = 0;118private _lastAcceptedCompletionTimestamp: number = 0;119private _lastUserData?: string;120121static requestCompletionsSequence = '\x1b[24~e'; // F12,e122123private readonly _onBell = this._register(new Emitter<void>());124readonly onBell = this._onBell.event;125private readonly _onAcceptedCompletion = this._register(new Emitter<string>());126readonly onAcceptedCompletion = this._onAcceptedCompletion.event;127128constructor(129private readonly _cachedPwshCommands: Set<SimpleCompletionItem>,130private readonly _capabilities: ITerminalCapabilityStore,131private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>,132@IConfigurationService private readonly _configurationService: IConfigurationService,133@IInstantiationService private readonly _instantiationService: IInstantiationService,134) {135super();136137this._register(Event.runAndSubscribe(Event.any(138this._capabilities.onDidAddCapabilityType,139this._capabilities.onDidRemoveCapabilityType140), () => {141const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection);142if (commandDetection) {143if (this._promptInputModel !== commandDetection.promptInputModel) {144this._promptInputModel = commandDetection.promptInputModel;145this._promptInputModelSubscriptions.value = combinedDisposable(146this._promptInputModel.onDidChangeInput(e => this._sync(e)),147this._promptInputModel.onDidFinishInput(() => this.hideSuggestWidget()),148);149}150} else {151this._promptInputModel = undefined;152}153}));154}155156activate(xterm: Terminal): void {157this._terminal = xterm;158this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.VSCode, data => {159return this._handleVSCodeSequence(data);160}));161this._register(xterm.onData(e => {162if (!e.startsWith('\x1b[')) {163this._lastUserData = e;164this._lastUserDataTimestamp = Date.now();165}166}));167}168169setPanel(panel: HTMLElement): void {170this._panel = panel;171}172173setScreen(screen: HTMLElement): void {174this._screen = screen;175}176177private _requestCompletions(): void {178// Ensure that a key has been pressed since the last accepted completion in order to prevent179// completions being requested again right after accepting a completion180if (this._lastUserDataTimestamp > this._lastAcceptedCompletionTimestamp) {181this._onAcceptedCompletion.fire(SuggestAddon.requestCompletionsSequence);182}183}184185private _sync(promptInputState: IPromptInputModelState): void {186const config = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection);187188if (!this._mostRecentPromptInputState || promptInputState.cursorIndex > this._mostRecentPromptInputState.cursorIndex) {189// If input has been added190let sent = false;191192// Quick suggestions193if (!this._terminalSuggestWidgetVisibleContextKey.get()) {194if (config.quickSuggestions) {195const completionPrefix = promptInputState.value.substring(0, promptInputState.cursorIndex);196if (promptInputState.cursorIndex === 1 || completionPrefix.match(/([\s\[])[^\s]$/)) {197// Never request completions if the last key sequence was up or down as the user was likely198// navigating history199if (this._lastUserData !== /*up*/'\x1b[A' && this._lastUserData !== /*down*/'\x1b[B') {200this._requestCompletions();201sent = true;202}203}204}205}206207// Trigger characters - this happens even if the widget is showing208if (config.suggestOnTriggerCharacters && !sent) {209const lastChar = promptInputState.value.at(promptInputState.cursorIndex - 1);210if (lastChar?.match(/[\\\/\-]/)) {211this._requestCompletions();212sent = true;213}214}215}216217this._mostRecentPromptInputState = promptInputState;218if (!this._promptInputModel || !this._terminal || !this._suggestWidget || !this._initialPromptInputState || this._leadingLineContent === undefined) {219return;220}221222this._currentPromptInputState = promptInputState;223224// Hide the widget if the latest character was a space225if (this._currentPromptInputState.cursorIndex > 1 && this._currentPromptInputState.value.at(this._currentPromptInputState.cursorIndex - 1) === ' ') {226this.hideSuggestWidget();227return;228}229230// Hide the widget if the cursor moves to the left of the initial position as the231// completions are no longer valid232if (this._currentPromptInputState.cursorIndex < this._initialPromptInputState.cursorIndex) {233this.hideSuggestWidget();234return;235}236237if (this._terminalSuggestWidgetVisibleContextKey.get()) {238this._cursorIndexDelta = this._currentPromptInputState.cursorIndex - this._initialPromptInputState.cursorIndex;239const lineContext = new LineContext(this._leadingLineContent + this._currentPromptInputState.value.substring(this._leadingLineContent.length, this._leadingLineContent.length + this._cursorIndexDelta), this._cursorIndexDelta);240this._suggestWidget.setLineContext(lineContext);241}242243// Hide and clear model if there are no more items244if (!this._suggestWidget.hasCompletions()) {245this.hideSuggestWidget();246return;247}248249const dimensions = this._getTerminalDimensions();250if (!dimensions.width || !dimensions.height) {251return;252}253// TODO: What do frozen and auto do?254const xtermBox = this._screen!.getBoundingClientRect();255256this._suggestWidget.showSuggestions(0, false, false, {257left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width,258top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height,259height: dimensions.height260});261}262263private _handleVSCodeSequence(data: string): boolean | Promise<boolean> {264if (!this._terminal) {265return false;266}267268// Pass the sequence along to the capability269const [command, ...args] = data.split(';');270switch (command) {271case VSCodeSuggestOscPt.Completions:272this._handleCompletionsSequence(this._terminal, data, command, args);273return true;274case VSCodeSuggestOscPt.CompletionsBash:275this._handleCompletionsBashSequence(this._terminal, data, command, args);276return true;277case VSCodeSuggestOscPt.CompletionsBashFirstWord:278return this._handleCompletionsBashFirstWordSequence(this._terminal, data, command, args);279}280281// Unrecognized sequence282return false;283}284285private _handleCompletionsSequence(terminal: Terminal, data: string, command: string, args: string[]): void {286// Nothing to handle if the terminal is not attached287if (!terminal.element || !this._enableWidget || !this._promptInputModel) {288return;289}290291let replacementIndex = 0;292let replacementLength = this._promptInputModel.cursorIndex;293294const payload = data.slice(command.length + args[0].length + args[1].length + args[2].length + 4/*semi-colons*/);295const rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion = args.length === 0 || payload.length === 0 ? undefined : JSON.parse(payload);296const completions = parseCompletionsFromShell(rawCompletions);297298this._leadingLineContent = this._promptInputModel.value.substring(0, this._promptInputModel.cursorIndex);299300const firstChar = this._leadingLineContent.length === 0 ? '' : this._leadingLineContent[0];301// This is a TabExpansion2 result302if (this._leadingLineContent.trim().includes(' ') || firstChar === '[') {303replacementIndex = parseInt(args[0]);304replacementLength = parseInt(args[1]);305this._leadingLineContent = this._promptInputModel.value.substring(0, this._promptInputModel.cursorIndex);306}307// This is a global command, add cached commands list to completions308else {309completions.push(...this._cachedPwshCommands);310}311312this._currentPromptInputState = {313value: this._promptInputModel.value,314cursorIndex: this._promptInputModel.cursorIndex,315ghostTextIndex: this._promptInputModel.ghostTextIndex316};317this._cursorIndexDelta = 0;318const lineContext = new LineContext(this._leadingLineContent + this._currentPromptInputState.value.substring(this._leadingLineContent.length, this._leadingLineContent.length + this._cursorIndexDelta), this._cursorIndexDelta);319const model = new SimpleCompletionModel(completions, lineContext, replacementIndex, replacementLength);320this._handleCompletionModel(model);321}322323// TODO: These aren't persisted across reloads324// TODO: Allow triggering anywhere in the first word based on the cached completions325private _cachedBashAliases: Set<SimpleCompletionItem> = new Set();326private _cachedBashBuiltins: Set<SimpleCompletionItem> = new Set();327private _cachedBashCommands: Set<SimpleCompletionItem> = new Set();328private _cachedBashKeywords: Set<SimpleCompletionItem> = new Set();329private _cachedFirstWord?: SimpleCompletionItem[];330private _handleCompletionsBashFirstWordSequence(terminal: Terminal, data: string, command: string, args: string[]): boolean {331const type = args[0];332const completionList: string[] = data.slice(command.length + type.length + 2/*semi-colons*/).split(';');333let set: Set<SimpleCompletionItem>;334switch (type) {335case 'alias': set = this._cachedBashAliases; break;336case 'builtin': set = this._cachedBashBuiltins; break;337case 'command': set = this._cachedBashCommands; break;338case 'keyword': set = this._cachedBashKeywords; break;339default: return false;340}341set.clear();342const distinctLabels: Set<string> = new Set();343for (const label of completionList) {344distinctLabels.add(label);345}346for (const label of distinctLabels) {347set.add(new SimpleCompletionItem({348label,349icon: Codicon.symbolString,350detail: type351}));352}353// Invalidate compound list cache354this._cachedFirstWord = undefined;355return true;356}357358private _handleCompletionsBashSequence(terminal: Terminal, data: string, command: string, args: string[]): void {359// Nothing to handle if the terminal is not attached360if (!terminal.element) {361return;362}363364let replacementIndex = parseInt(args[0]);365const replacementLength = parseInt(args[1]);366if (!args[2]) {367this._onBell.fire();368return;369}370371const completionList: string[] = data.slice(command.length + args[0].length + args[1].length + args[2].length + 4/*semi-colons*/).split(';');372// TODO: Create a trigger suggest command which encapsulates sendSequence and uses cached if available373let completions: SimpleCompletionItem[];374// TODO: This 100 is a hack just for the prototype, this should get it based on some terminal input model375if (replacementIndex !== 100 && completionList.length > 0) {376completions = completionList.map(label => {377return new SimpleCompletionItem({378label: label,379icon: Codicon.symbolProperty380});381});382} else {383replacementIndex = 0;384if (!this._cachedFirstWord) {385this._cachedFirstWord = [386...this._cachedBashAliases,387...this._cachedBashBuiltins,388...this._cachedBashCommands,389...this._cachedBashKeywords390];391this._cachedFirstWord.sort((a, b) => {392const aCode = a.completion.label.charCodeAt(0);393const bCode = b.completion.label.charCodeAt(0);394const isANonAlpha = aCode < 65 || aCode > 90 && aCode < 97 || aCode > 122 ? 1 : 0;395const isBNonAlpha = bCode < 65 || bCode > 90 && bCode < 97 || bCode > 122 ? 1 : 0;396if (isANonAlpha !== isBNonAlpha) {397return isANonAlpha - isBNonAlpha;398}399return a.completion.label.localeCompare(b.completion.label);400});401}402completions = this._cachedFirstWord;403}404if (completions.length === 0) {405return;406}407408this._leadingLineContent = completions[0].completion.label.slice(0, replacementLength);409const model = new SimpleCompletionModel(completions, new LineContext(this._leadingLineContent, replacementIndex), replacementIndex, replacementLength);410if (completions.length === 1) {411const insertText = completions[0].completion.label.substring(replacementLength);412if (insertText.length === 0) {413this._onBell.fire();414return;415}416}417this._handleCompletionModel(model);418}419420private _getTerminalDimensions(): { width: number; height: number } {421const cssCellDims = (this._terminal as any as { _core: IXtermCore })._core._renderService.dimensions.css.cell;422return {423width: cssCellDims.width,424height: cssCellDims.height,425};426}427428private _handleCompletionModel(model: SimpleCompletionModel): void {429if (model.items.length === 0 || !this._terminal?.element || !this._promptInputModel) {430return;431}432this._model = model;433const suggestWidget = this._ensureSuggestWidget(this._terminal);434const dimensions = this._getTerminalDimensions();435if (!dimensions.width || !dimensions.height) {436return;437}438// TODO: What do frozen and auto do?439const xtermBox = this._screen!.getBoundingClientRect();440this._initialPromptInputState = {441value: this._promptInputModel.value,442cursorIndex: this._promptInputModel.cursorIndex,443ghostTextIndex: this._promptInputModel.ghostTextIndex444};445suggestWidget.setCompletionModel(model);446suggestWidget.showSuggestions(0, false, false, {447left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width,448top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height,449height: dimensions.height450});451}452453private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget {454this._terminalSuggestWidgetVisibleContextKey.set(true);455if (!this._suggestWidget) {456this._suggestWidget = this._register(this._instantiationService.createInstance(457SimpleSuggestWidget,458this._panel!,459this._instantiationService.createInstance(PersistedWidgetSize),460{}461));462this._suggestWidget.list.style(getListStyles({463listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,464listInactiveFocusOutline: activeContrastBorder465}));466this._register(this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e)));467this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false)));468this._register(this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true)));469}470return this._suggestWidget;471}472473selectPreviousSuggestion(): void {474this._suggestWidget?.selectPrevious();475}476477selectPreviousPageSuggestion(): void {478this._suggestWidget?.selectPreviousPage();479}480481selectNextSuggestion(): void {482this._suggestWidget?.selectNext();483}484485selectNextPageSuggestion(): void {486this._suggestWidget?.selectNextPage();487}488489acceptSelectedSuggestion(suggestion?: Pick<ISimpleSelectedSuggestion, 'item' | 'model'>, respectRunOnEnter?: boolean): void {490if (!suggestion) {491suggestion = this._suggestWidget?.getFocusedItem();492}493const initialPromptInputState = this._initialPromptInputState ?? this._mostRecentPromptInputState;494if (!suggestion || !initialPromptInputState || !this._leadingLineContent || !this._model) {495return;496}497this._lastAcceptedCompletionTimestamp = Date.now();498this._suggestWidget?.hide();499500const currentPromptInputState = this._currentPromptInputState ?? initialPromptInputState;501502// The replacement text is any text after the replacement index for the completions, this503// includes any text that was there before the completions were requested and any text added504// since to refine the completion.505const replacementText = currentPromptInputState.value.substring(this._model.replacementIndex, currentPromptInputState.cursorIndex);506507// Right side of replacement text in the same word508let rightSideReplacementText = '';509if (510// The line didn't end with ghost text511(currentPromptInputState.ghostTextIndex === -1 || currentPromptInputState.ghostTextIndex > currentPromptInputState.cursorIndex) &&512// There is more than one charatcer513currentPromptInputState.value.length > currentPromptInputState.cursorIndex + 1 &&514// THe next character is not a space515currentPromptInputState.value.at(currentPromptInputState.cursorIndex) !== ' '516) {517const spaceIndex = currentPromptInputState.value.substring(currentPromptInputState.cursorIndex, currentPromptInputState.ghostTextIndex === -1 ? undefined : currentPromptInputState.ghostTextIndex).indexOf(' ');518rightSideReplacementText = currentPromptInputState.value.substring(currentPromptInputState.cursorIndex, spaceIndex === -1 ? undefined : currentPromptInputState.cursorIndex + spaceIndex);519}520521const completion = suggestion.item.completion;522const completionText = completion.label;523524let runOnEnter = false;525if (respectRunOnEnter) {526const runOnEnterConfig = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection).runOnEnter;527switch (runOnEnterConfig) {528case 'always': {529runOnEnter = true;530break;531}532case 'exactMatch': {533runOnEnter = replacementText.toLowerCase() === completionText.toLowerCase();534break;535}536case 'exactMatchIgnoreExtension': {537runOnEnter = replacementText.toLowerCase() === completionText.toLowerCase();538if (completion.icon === Codicon.symbolFile || completion.icon === Codicon.symbolMethod) {539runOnEnter ||= replacementText.toLowerCase() === completionText.toLowerCase().replace(/\.[^\.]+$/, '');540}541break;542}543}544}545546// For folders, allow the next completion request to get completions for that folder547if (completion.icon === Codicon.folder) {548this._lastAcceptedCompletionTimestamp = 0;549}550551552553// Send the completion554this._onAcceptedCompletion.fire([555// Backspace (left) to remove all additional input556'\x7F'.repeat(replacementText.length),557// Delete (right) to remove any additional text in the same word558'\x1b[3~'.repeat(rightSideReplacementText.length),559// Write the completion560completion.label,561// Run on enter if needed562runOnEnter ? '\r' : ''563].join(''));564565this.hideSuggestWidget();566}567568hideSuggestWidget(): void {569this._initialPromptInputState = undefined;570this._currentPromptInputState = undefined;571this._suggestWidget?.hide();572}573}574575class PersistedWidgetSize {576577private readonly _key = TerminalStorageKeys.TerminalSuggestSize;578579constructor(580@IStorageService private readonly _storageService: IStorageService581) {582}583584restore(): dom.Dimension | undefined {585const raw = this._storageService.get(this._key, StorageScope.PROFILE) ?? '';586try {587const obj = JSON.parse(raw);588if (dom.Dimension.is(obj)) {589return dom.Dimension.lift(obj);590}591} catch {592// ignore593}594return undefined;595}596597store(size: dom.Dimension) {598this._storageService.store(this._key, JSON.stringify(size), StorageScope.PROFILE, StorageTarget.MACHINE);599}600601reset(): void {602this._storageService.remove(this._key, StorageScope.PROFILE);603}604}605606export function parseCompletionsFromShell(rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion) {607if (!rawCompletions) {608return [];609}610if (!Array.isArray(rawCompletions)) {611return [rawCompletions].map(e => (new SimpleCompletionItem({612label: e.CompletionText,613icon: pwshTypeToIconMap[e.ResultType],614detail: e.ToolTip,615isFile: e.ResultType === 3,616})));617}618if (rawCompletions.length === 0) {619return [];620}621if (typeof rawCompletions[0] === 'string') {622return [rawCompletions as CompressedPwshCompletion].map(e => (new SimpleCompletionItem({623label: e[0],624icon: pwshTypeToIconMap[e[1]],625detail: e[2],626isFile: e[1] === 3,627})));628}629if (Array.isArray(rawCompletions[0])) {630return (rawCompletions as CompressedPwshCompletion[]).map(e => (new SimpleCompletionItem({631label: e[0],632icon: pwshTypeToIconMap[e[1]],633detail: e[2],634isFile: e[1] === 3,635})));636}637return (rawCompletions as PwshCompletion[]).map(e => (new SimpleCompletionItem({638label: e.CompletionText,639icon: pwshTypeToIconMap[e.ResultType],640detail: e.ToolTip,641isFile: e.ResultType === 3,642})));643}644645646