UNPKG

monaco-editor-core

Version:

A browser based code editor

729 lines (728 loc) • 36.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import * as dom from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { getBaseLayerHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate2.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { List } from '../../../../base/browser/ui/list/listWidget.js'; import * as arrays from '../../../../base/common/arrays.js'; import { DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter } from '../../../../base/common/event.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType, isDefined } from '../../../../base/common/types.js'; import './renameWidget.css'; import * as domFontInfo from '../../../browser/config/domFontInfo.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; import { NewSymbolNameTag, NewSymbolNameTriggerKind } from '../../../common/languages.js'; import * as nls from '../../../../nls.js'; import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { getListStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { editorWidgetBackground, inputBackground, inputBorder, inputForeground, quickInputListFocusBackground, quickInputListFocusForeground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; /** for debugging */ const _sticky = false; export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false, nls.localize('renameInputVisible', "Whether the rename input widget is visible")); export const CONTEXT_RENAME_INPUT_FOCUSED = new RawContextKey('renameInputFocused', false, nls.localize('renameInputFocused', "Whether the rename input widget is focused")); let RenameWidget = class RenameWidget { constructor(_editor, _acceptKeybindings, _themeService, _keybindingService, contextKeyService, _logService) { this._editor = _editor; this._acceptKeybindings = _acceptKeybindings; this._themeService = _themeService; this._keybindingService = _keybindingService; this._logService = _logService; // implement IContentWidget this.allowEditorOverflow = true; this._disposables = new DisposableStore(); this._visibleContextKey = CONTEXT_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); this._isEditingRenameCandidate = false; this._nRenameSuggestionsInvocations = 0; this._hadAutomaticRenameSuggestionsInvocation = false; this._candidates = new Set(); this._beforeFirstInputFieldEditSW = new StopWatch(); this._inputWithButton = new InputWithButton(); this._disposables.add(this._inputWithButton); this._editor.addContentWidget(this); this._disposables.add(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(50 /* EditorOption.fontInfo */)) { this._updateFont(); } })); this._disposables.add(_themeService.onDidColorThemeChange(this._updateStyles, this)); } dispose() { this._disposables.dispose(); this._editor.removeContentWidget(this); } getId() { return '__renameInputWidget'; } getDomNode() { if (!this._domNode) { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-editor rename-box'; this._domNode.appendChild(this._inputWithButton.domNode); this._renameCandidateListView = this._disposables.add(new RenameCandidateListView(this._domNode, { fontInfo: this._editor.getOption(50 /* EditorOption.fontInfo */), onFocusChange: (newSymbolName) => { this._inputWithButton.input.value = newSymbolName; this._isEditingRenameCandidate = false; // @ulugbekna: reset }, onSelectionChange: () => { this._isEditingRenameCandidate = false; // @ulugbekna: because user picked a rename suggestion this.acceptInput(false); // we don't allow preview with mouse click for now } })); this._disposables.add(this._inputWithButton.onDidInputChange(() => { if (this._renameCandidateListView?.focusedCandidate !== undefined) { this._isEditingRenameCandidate = true; } this._timeBeforeFirstInputFieldEdit ??= this._beforeFirstInputFieldEditSW.elapsed(); if (this._renameCandidateProvidersCts?.token.isCancellationRequested === false) { this._renameCandidateProvidersCts.cancel(); } this._renameCandidateListView?.clearFocus(); })); this._label = document.createElement('div'); this._label.className = 'rename-label'; this._domNode.appendChild(this._label); this._updateFont(); this._updateStyles(this._themeService.getColorTheme()); } return this._domNode; } _updateStyles(theme) { if (!this._domNode) { return; } const widgetShadowColor = theme.getColor(widgetShadow); const widgetBorderColor = theme.getColor(widgetBorder); this._domNode.style.backgroundColor = String(theme.getColor(editorWidgetBackground) ?? ''); this._domNode.style.boxShadow = widgetShadowColor ? ` 0 0 8px 2px ${widgetShadowColor}` : ''; this._domNode.style.border = widgetBorderColor ? `1px solid ${widgetBorderColor}` : ''; this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); const border = theme.getColor(inputBorder); this._inputWithButton.domNode.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); this._inputWithButton.input.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); this._inputWithButton.domNode.style.borderWidth = border ? '1px' : '0px'; this._inputWithButton.domNode.style.borderStyle = border ? 'solid' : 'none'; this._inputWithButton.domNode.style.borderColor = border?.toString() ?? 'none'; } _updateFont() { if (this._domNode === undefined) { return; } assertType(this._label !== undefined, 'RenameWidget#_updateFont: _label must not be undefined given _domNode is defined'); this._editor.applyFontInfo(this._inputWithButton.input); const fontInfo = this._editor.getOption(50 /* EditorOption.fontInfo */); this._label.style.fontSize = `${this._computeLabelFontSize(fontInfo.fontSize)}px`; } _computeLabelFontSize(editorFontSize) { return editorFontSize * 0.8; } getPosition() { if (!this._visible) { return null; } if (!this._editor.hasModel() || // @ulugbekna: shouldn't happen !this._editor.getDomNode() // @ulugbekna: can happen during tests based on suggestWidget's similar predicate check ) { return null; } const bodyBox = dom.getClientArea(this.getDomNode().ownerDocument.body); const editorBox = dom.getDomNodePagePosition(this._editor.getDomNode()); const cursorBoxTop = this._getTopForPosition(); this._nPxAvailableAbove = cursorBoxTop + editorBox.top; this._nPxAvailableBelow = bodyBox.height - this._nPxAvailableAbove; const lineHeight = this._editor.getOption(67 /* EditorOption.lineHeight */); const { totalHeight: candidateViewHeight } = RenameCandidateView.getLayoutInfo({ lineHeight }); const positionPreference = this._nPxAvailableBelow > candidateViewHeight * 6 /* approximate # of candidates to fit in (inclusive of rename input box & rename label) */ ? [2 /* ContentWidgetPositionPreference.BELOW */, 1 /* ContentWidgetPositionPreference.ABOVE */] : [1 /* ContentWidgetPositionPreference.ABOVE */, 2 /* ContentWidgetPositionPreference.BELOW */]; return { position: this._position, preference: positionPreference, }; } beforeRender() { const [accept, preview] = this._acceptKeybindings; this._label.innerText = nls.localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); this._domNode.style.minWidth = `200px`; // to prevent from widening when candidates come in return null; } afterRender(position) { // FIXME@ulugbekna: commenting trace log out until we start unmounting the widget from editor properly - https://github.com/microsoft/vscode/issues/226975 // this._trace('invoking afterRender, position: ', position ? 'not null' : 'null'); if (position === null) { // cancel rename when input widget isn't rendered anymore this.cancelInput(true, 'afterRender (because position is null)'); return; } if (!this._editor.hasModel() || // shouldn't happen !this._editor.getDomNode() // can happen during tests based on suggestWidget's similar predicate check ) { return; } assertType(this._renameCandidateListView); assertType(this._nPxAvailableAbove !== undefined); assertType(this._nPxAvailableBelow !== undefined); const inputBoxHeight = dom.getTotalHeight(this._inputWithButton.domNode); const labelHeight = dom.getTotalHeight(this._label); let totalHeightAvailable; if (position === 2 /* ContentWidgetPositionPreference.BELOW */) { totalHeightAvailable = this._nPxAvailableBelow; } else { totalHeightAvailable = this._nPxAvailableAbove; } this._renameCandidateListView.layout({ height: totalHeightAvailable - labelHeight - inputBoxHeight, width: dom.getTotalWidth(this._inputWithButton.domNode), }); } acceptInput(wantsPreview) { this._trace(`invoking acceptInput`); this._currentAcceptInput?.(wantsPreview); } cancelInput(focusEditor, caller) { // this._trace(`invoking cancelInput, caller: ${caller}, _currentCancelInput: ${this._currentAcceptInput ? 'not undefined' : 'undefined'}`); this._currentCancelInput?.(focusEditor); } focusNextRenameSuggestion() { if (!this._renameCandidateListView?.focusNext()) { this._inputWithButton.input.value = this._currentName; } } focusPreviousRenameSuggestion() { if (!this._renameCandidateListView?.focusPrevious()) { this._inputWithButton.input.value = this._currentName; } } /** * @param requestRenameCandidates is `undefined` when there are no rename suggestion providers */ getInput(where, currentName, supportPreview, requestRenameCandidates, cts) { const { start: selectionStart, end: selectionEnd } = this._getSelection(where, currentName); this._renameCts = cts; const disposeOnDone = new DisposableStore(); this._nRenameSuggestionsInvocations = 0; this._hadAutomaticRenameSuggestionsInvocation = false; if (requestRenameCandidates === undefined) { this._inputWithButton.button.style.display = 'none'; } else { this._inputWithButton.button.style.display = 'flex'; this._requestRenameCandidatesOnce = requestRenameCandidates; this._requestRenameCandidates(currentName, false); disposeOnDone.add(dom.addDisposableListener(this._inputWithButton.button, 'click', () => this._requestRenameCandidates(currentName, true))); disposeOnDone.add(dom.addDisposableListener(this._inputWithButton.button, dom.EventType.KEY_DOWN, (e) => { const keyEvent = new StandardKeyboardEvent(e); if (keyEvent.equals(3 /* KeyCode.Enter */) || keyEvent.equals(10 /* KeyCode.Space */)) { keyEvent.stopPropagation(); keyEvent.preventDefault(); this._requestRenameCandidates(currentName, true); } })); } this._isEditingRenameCandidate = false; this._domNode.classList.toggle('preview', supportPreview); this._position = new Position(where.startLineNumber, where.startColumn); this._currentName = currentName; this._inputWithButton.input.value = currentName; this._inputWithButton.input.setAttribute('selectionStart', selectionStart.toString()); this._inputWithButton.input.setAttribute('selectionEnd', selectionEnd.toString()); this._inputWithButton.input.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); // determines width this._beforeFirstInputFieldEditSW.reset(); disposeOnDone.add(toDisposable(() => { this._renameCts = undefined; cts.dispose(true); })); // @ulugbekna: this may result in `this.cancelInput` being called twice, but it should be safe since we set it to undefined after 1st call disposeOnDone.add(toDisposable(() => { if (this._renameCandidateProvidersCts !== undefined) { this._renameCandidateProvidersCts.dispose(true); this._renameCandidateProvidersCts = undefined; } })); disposeOnDone.add(toDisposable(() => this._candidates.clear())); const inputResult = new DeferredPromise(); inputResult.p.finally(() => { disposeOnDone.dispose(); this._hide(); }); this._currentCancelInput = (focusEditor) => { this._trace('invoking _currentCancelInput'); this._currentAcceptInput = undefined; this._currentCancelInput = undefined; // fixme session cleanup this._renameCandidateListView?.clearCandidates(); inputResult.complete(focusEditor); return true; }; this._currentAcceptInput = (wantsPreview) => { this._trace('invoking _currentAcceptInput'); assertType(this._renameCandidateListView !== undefined); const nRenameSuggestions = this._renameCandidateListView.nCandidates; let newName; let source; const focusedCandidate = this._renameCandidateListView.focusedCandidate; if (focusedCandidate !== undefined) { this._trace('using new name from renameSuggestion'); newName = focusedCandidate; source = { k: 'renameSuggestion' }; } else { this._trace('using new name from inputField'); newName = this._inputWithButton.input.value; source = this._isEditingRenameCandidate ? { k: 'userEditedRenameSuggestion' } : { k: 'inputField' }; } if (newName === currentName || newName.trim().length === 0 /* is just whitespace */) { this.cancelInput(true, '_currentAcceptInput (because newName === value || newName.trim().length === 0)'); return; } this._currentAcceptInput = undefined; this._currentCancelInput = undefined; this._renameCandidateListView.clearCandidates(); // fixme session cleanup inputResult.complete({ newName, wantsPreview: supportPreview && wantsPreview, stats: { source, nRenameSuggestions, timeBeforeFirstInputFieldEdit: this._timeBeforeFirstInputFieldEdit, nRenameSuggestionsInvocations: this._nRenameSuggestionsInvocations, hadAutomaticRenameSuggestionsInvocation: this._hadAutomaticRenameSuggestionsInvocation, } }); }; disposeOnDone.add(cts.token.onCancellationRequested(() => this.cancelInput(true, 'cts.token.onCancellationRequested'))); if (!_sticky) { disposeOnDone.add(this._editor.onDidBlurEditorWidget(() => this.cancelInput(!this._domNode?.ownerDocument.hasFocus(), 'editor.onDidBlurEditorWidget'))); } this._show(); return inputResult.p; } _requestRenameCandidates(currentName, isManuallyTriggered) { if (this._requestRenameCandidatesOnce === undefined) { return; } if (this._renameCandidateProvidersCts !== undefined) { this._renameCandidateProvidersCts.dispose(true); } assertType(this._renameCts); if (this._inputWithButton.buttonState !== 'stop') { this._renameCandidateProvidersCts = new CancellationTokenSource(); const triggerKind = isManuallyTriggered ? NewSymbolNameTriggerKind.Invoke : NewSymbolNameTriggerKind.Automatic; const candidates = this._requestRenameCandidatesOnce(triggerKind, this._renameCandidateProvidersCts.token); if (candidates.length === 0) { this._inputWithButton.setSparkleButton(); return; } if (!isManuallyTriggered) { this._hadAutomaticRenameSuggestionsInvocation = true; } this._nRenameSuggestionsInvocations += 1; this._inputWithButton.setStopButton(); this._updateRenameCandidates(candidates, currentName, this._renameCts.token); } } /** * This allows selecting only part of the symbol name in the input field based on the selection in the editor */ _getSelection(where, currentName) { assertType(this._editor.hasModel()); const selection = this._editor.getSelection(); let start = 0; let end = currentName.length; if (!Range.isEmpty(selection) && !Range.spansMultipleLines(selection) && Range.containsRange(where, selection)) { start = Math.max(0, selection.startColumn - where.startColumn); end = Math.min(where.endColumn, selection.endColumn) - where.startColumn; } return { start, end }; } _show() { this._trace('invoking _show'); this._editor.revealLineInCenterIfOutsideViewport(this._position.lineNumber, 0 /* ScrollType.Smooth */); this._visible = true; this._visibleContextKey.set(true); this._editor.layoutContentWidget(this); // TODO@ulugbekna: could this be simply run in `afterRender`? setTimeout(() => { this._inputWithButton.input.focus(); this._inputWithButton.input.setSelectionRange(parseInt(this._inputWithButton.input.getAttribute('selectionStart')), parseInt(this._inputWithButton.input.getAttribute('selectionEnd'))); }, 100); } async _updateRenameCandidates(candidates, currentName, token) { const trace = (...args) => this._trace('_updateRenameCandidates', ...args); trace('start'); const namesListResults = await raceCancellation(Promise.allSettled(candidates), token); this._inputWithButton.setSparkleButton(); if (namesListResults === undefined) { trace('returning early - received updateRenameCandidates results - undefined'); return; } const newNames = namesListResults.flatMap(namesListResult => namesListResult.status === 'fulfilled' && isDefined(namesListResult.value) ? namesListResult.value : []); trace(`received updateRenameCandidates results - total (unfiltered) ${newNames.length} candidates.`); // deduplicate and filter out the current value const distinctNames = arrays.distinct(newNames, v => v.newSymbolName); trace(`distinct candidates - ${distinctNames.length} candidates.`); const validDistinctNames = distinctNames.filter(({ newSymbolName }) => newSymbolName.trim().length > 0 && newSymbolName !== this._inputWithButton.input.value && newSymbolName !== currentName && !this._candidates.has(newSymbolName)); trace(`valid distinct candidates - ${newNames.length} candidates.`); validDistinctNames.forEach(n => this._candidates.add(n.newSymbolName)); if (validDistinctNames.length < 1) { trace('returning early - no valid distinct candidates'); return; } // show the candidates trace('setting candidates'); this._renameCandidateListView.setCandidates(validDistinctNames); // ask editor to re-layout given that the widget is now of a different size after rendering rename candidates trace('asking editor to re-layout'); this._editor.layoutContentWidget(this); } _hide() { this._trace('invoked _hide'); this._visible = false; this._visibleContextKey.reset(); this._editor.layoutContentWidget(this); } _getTopForPosition() { const visibleRanges = this._editor.getVisibleRanges(); let firstLineInViewport; if (visibleRanges.length > 0) { firstLineInViewport = visibleRanges[0].startLineNumber; } else { this._logService.warn('RenameWidget#_getTopForPosition: this should not happen - visibleRanges is empty'); firstLineInViewport = Math.max(1, this._position.lineNumber - 5); // @ulugbekna: fallback to current line minus 5 } return this._editor.getTopForLineNumber(this._position.lineNumber) - this._editor.getTopForLineNumber(firstLineInViewport); } _trace(...args) { this._logService.trace('RenameWidget', ...args); } }; RenameWidget = __decorate([ __param(2, IThemeService), __param(3, IKeybindingService), __param(4, IContextKeyService), __param(5, ILogService) ], RenameWidget); export { RenameWidget }; class RenameCandidateListView { // FIXME@ulugbekna: rewrite using event emitters constructor(parent, opts) { this._disposables = new DisposableStore(); this._availableHeight = 0; this._minimumWidth = 0; this._lineHeight = opts.fontInfo.lineHeight; this._typicalHalfwidthCharacterWidth = opts.fontInfo.typicalHalfwidthCharacterWidth; this._listContainer = document.createElement('div'); this._listContainer.className = 'rename-box rename-candidate-list-container'; parent.appendChild(this._listContainer); this._listWidget = RenameCandidateListView._createListWidget(this._listContainer, this._candidateViewHeight, opts.fontInfo); this._listWidget.onDidChangeFocus(e => { if (e.elements.length === 1) { opts.onFocusChange(e.elements[0].newSymbolName); } }, this._disposables); this._listWidget.onDidChangeSelection(e => { if (e.elements.length === 1) { opts.onSelectionChange(); } }, this._disposables); this._disposables.add(this._listWidget.onDidBlur(e => { this._listWidget.setFocus([]); })); this._listWidget.style(getListStyles({ listInactiveFocusForeground: quickInputListFocusForeground, listInactiveFocusBackground: quickInputListFocusBackground, })); } dispose() { this._listWidget.dispose(); this._disposables.dispose(); } // height - max height allowed by parent element layout({ height, width }) { this._availableHeight = height; this._minimumWidth = width; } setCandidates(candidates) { // insert candidates into list widget this._listWidget.splice(0, 0, candidates); // adjust list widget layout const height = this._pickListHeight(this._listWidget.length); const width = this._pickListWidth(candidates); this._listWidget.layout(height, width); // adjust list container layout this._listContainer.style.height = `${height}px`; this._listContainer.style.width = `${width}px`; aria.status(nls.localize('renameSuggestionsReceivedAria', "Received {0} rename suggestions", candidates.length)); } clearCandidates() { this._listContainer.style.height = '0px'; this._listContainer.style.width = '0px'; this._listWidget.splice(0, this._listWidget.length, []); } get nCandidates() { return this._listWidget.length; } get focusedCandidate() { if (this._listWidget.length === 0) { return; } const selectedElement = this._listWidget.getSelectedElements()[0]; if (selectedElement !== undefined) { return selectedElement.newSymbolName; } const focusedElement = this._listWidget.getFocusedElements()[0]; if (focusedElement !== undefined) { return focusedElement.newSymbolName; } return; } focusNext() { if (this._listWidget.length === 0) { return false; } const focusedIxs = this._listWidget.getFocus(); if (focusedIxs.length === 0) { this._listWidget.focusFirst(); this._listWidget.reveal(0); return true; } else { if (focusedIxs[0] === this._listWidget.length - 1) { this._listWidget.setFocus([]); this._listWidget.reveal(0); // @ulugbekna: without this, it seems like focused element is obstructed return false; } else { this._listWidget.focusNext(); const focused = this._listWidget.getFocus()[0]; this._listWidget.reveal(focused); return true; } } } /** * @returns true if focus is moved to previous element */ focusPrevious() { if (this._listWidget.length === 0) { return false; } const focusedIxs = this._listWidget.getFocus(); if (focusedIxs.length === 0) { this._listWidget.focusLast(); const focused = this._listWidget.getFocus()[0]; this._listWidget.reveal(focused); return true; } else { if (focusedIxs[0] === 0) { this._listWidget.setFocus([]); return false; } else { this._listWidget.focusPrevious(); const focused = this._listWidget.getFocus()[0]; this._listWidget.reveal(focused); return true; } } } clearFocus() { this._listWidget.setFocus([]); } get _candidateViewHeight() { const { totalHeight } = RenameCandidateView.getLayoutInfo({ lineHeight: this._lineHeight }); return totalHeight; } _pickListHeight(nCandidates) { const heightToFitAllCandidates = this._candidateViewHeight * nCandidates; const MAX_N_CANDIDATES = 7; // @ulugbekna: max # of candidates we want to show at once const height = Math.min(heightToFitAllCandidates, this._availableHeight, this._candidateViewHeight * MAX_N_CANDIDATES); return height; } _pickListWidth(candidates) { const longestCandidateWidth = Math.ceil(Math.max(...candidates.map(c => c.newSymbolName.length)) * this._typicalHalfwidthCharacterWidth); const width = Math.max(this._minimumWidth, 4 /* padding */ + 16 /* sparkle icon */ + 5 /* margin-left */ + longestCandidateWidth + 10 /* (possibly visible) scrollbar width */ // TODO@ulugbekna: approximate calc - clean this up ); return width; } static _createListWidget(container, candidateViewHeight, fontInfo) { const virtualDelegate = new class { getTemplateId(element) { return 'candidate'; } getHeight(element) { return candidateViewHeight; } }; const renderer = new class { constructor() { this.templateId = 'candidate'; } renderTemplate(container) { return new RenameCandidateView(container, fontInfo); } renderElement(candidate, index, templateData) { templateData.populate(candidate); } disposeTemplate(templateData) { templateData.dispose(); } }; return new List('NewSymbolNameCandidates', container, virtualDelegate, [renderer], { keyboardSupport: false, // @ulugbekna: because we handle keyboard events through proper commands & keybinding service, see `rename.ts` mouseSupport: true, multipleSelectionSupport: false, }); } } class InputWithButton { constructor() { this._onDidInputChange = new Emitter(); this.onDidInputChange = this._onDidInputChange.event; this._disposables = new DisposableStore(); } get domNode() { if (!this._domNode) { this._domNode = document.createElement('div'); this._domNode.className = 'rename-input-with-button'; this._domNode.style.display = 'flex'; this._domNode.style.flexDirection = 'row'; this._domNode.style.alignItems = 'center'; this._inputNode = document.createElement('input'); this._inputNode.className = 'rename-input'; this._inputNode.type = 'text'; this._inputNode.style.border = 'none'; this._inputNode.setAttribute('aria-label', nls.localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); this._domNode.appendChild(this._inputNode); this._buttonNode = document.createElement('div'); this._buttonNode.className = 'rename-suggestions-button'; this._buttonNode.setAttribute('tabindex', '0'); this._buttonGenHoverText = nls.localize('generateRenameSuggestionsButton', "Generate new name suggestions"); this._buttonCancelHoverText = nls.localize('cancelRenameSuggestionsButton', "Cancel"); this._buttonHover = getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('element'), this._buttonNode, this._buttonGenHoverText); this._disposables.add(this._buttonHover); this._domNode.appendChild(this._buttonNode); // notify if selection changes to cancel request to rename-suggestion providers this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.INPUT, () => this._onDidInputChange.fire())); this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.KEY_DOWN, (e) => { const keyEvent = new StandardKeyboardEvent(e); if (keyEvent.keyCode === 15 /* KeyCode.LeftArrow */ || keyEvent.keyCode === 17 /* KeyCode.RightArrow */) { this._onDidInputChange.fire(); } })); this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.CLICK, () => this._onDidInputChange.fire())); // focus "container" border instead of input box this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.FOCUS, () => { this.domNode.style.outlineWidth = '1px'; this.domNode.style.outlineStyle = 'solid'; this.domNode.style.outlineOffset = '-1px'; this.domNode.style.outlineColor = 'var(--vscode-focusBorder)'; })); this._disposables.add(dom.addDisposableListener(this.input, dom.EventType.BLUR, () => { this.domNode.style.outline = 'none'; })); } return this._domNode; } get input() { assertType(this._inputNode); return this._inputNode; } get button() { assertType(this._buttonNode); return this._buttonNode; } get buttonState() { return this._buttonState; } setSparkleButton() { this._buttonState = 'sparkle'; this._sparkleIcon ??= renderIcon(Codicon.sparkle); dom.clearNode(this.button); this.button.appendChild(this._sparkleIcon); this.button.setAttribute('aria-label', 'Generating new name suggestions'); this._buttonHover?.update(this._buttonGenHoverText); this.input.focus(); } setStopButton() { this._buttonState = 'stop'; this._stopIcon ??= renderIcon(Codicon.primitiveSquare); dom.clearNode(this.button); this.button.appendChild(this._stopIcon); this.button.setAttribute('aria-label', 'Cancel generating new name suggestions'); this._buttonHover?.update(this._buttonCancelHoverText); this.input.focus(); } dispose() { this._disposables.dispose(); } } class RenameCandidateView { static { this._PADDING = 2; } constructor(parent, fontInfo) { this._domNode = document.createElement('div'); this._domNode.className = 'rename-box rename-candidate'; this._domNode.style.display = `flex`; this._domNode.style.columnGap = `5px`; this._domNode.style.alignItems = `center`; this._domNode.style.height = `${fontInfo.lineHeight}px`; this._domNode.style.padding = `${RenameCandidateView._PADDING}px`; // @ulugbekna: needed to keep space when the `icon.style.display` is set to `none` const iconContainer = document.createElement('div'); iconContainer.style.display = `flex`; iconContainer.style.alignItems = `center`; iconContainer.style.width = iconContainer.style.height = `${fontInfo.lineHeight * 0.8}px`; this._domNode.appendChild(iconContainer); this._icon = renderIcon(Codicon.sparkle); this._icon.style.display = `none`; iconContainer.appendChild(this._icon); this._label = document.createElement('div'); domFontInfo.applyFontInfo(this._label, fontInfo); this._domNode.appendChild(this._label); parent.appendChild(this._domNode); } populate(value) { this._updateIcon(value); this._updateLabel(value); } _updateIcon(value) { const isAIGenerated = !!value.tags?.includes(NewSymbolNameTag.AIGenerated); this._icon.style.display = isAIGenerated ? 'inherit' : 'none'; } _updateLabel(value) { this._label.innerText = value.newSymbolName; } static getLayoutInfo({ lineHeight }) { const totalHeight = lineHeight + RenameCandidateView._PADDING * 2 /* top & bottom padding */; return { totalHeight }; } dispose() { } }