monaco-editor-core
Version:
A browser based code editor
729 lines (728 loc) • 36.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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() {
}
}