suggestWidget.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. 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;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  15. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  16. return new (P || (P = Promise))(function (resolve, reject) {
  17. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  18. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  19. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  20. step((generator = generator.apply(thisArg, _arguments || [])).next());
  21. });
  22. };
  23. import * as dom from '../../../base/browser/dom.js';
  24. import '../../../base/browser/ui/codicons/codiconStyles.js'; // The codicon symbol styles are defined here and must be loaded
  25. import { List } from '../../../base/browser/ui/list/listWidget.js';
  26. import { createCancelablePromise, disposableTimeout, TimeoutTimer } from '../../../base/common/async.js';
  27. import { onUnexpectedError } from '../../../base/common/errors.js';
  28. import { Emitter } from '../../../base/common/event.js';
  29. import { DisposableStore } from '../../../base/common/lifecycle.js';
  30. import { clamp } from '../../../base/common/numbers.js';
  31. import * as strings from '../../../base/common/strings.js';
  32. import './media/suggest.css';
  33. import { EmbeddedCodeEditorWidget } from '../../browser/widget/embeddedCodeEditorWidget.js';
  34. import { SuggestWidgetStatus } from './suggestWidgetStatus.js';
  35. import '../symbolIcons/symbolIcons.js'; // The codicon symbol colors are defined here and must be loaded to get colors
  36. import * as nls from '../../../nls.js';
  37. import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
  38. import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
  39. import { IStorageService } from '../../../platform/storage/common/storage.js';
  40. import { activeContrastBorder, editorForeground, editorWidgetBackground, editorWidgetBorder, listFocusHighlightForeground, listHighlightForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, registerColor, transparent } from '../../../platform/theme/common/colorRegistry.js';
  41. import { attachListStyler } from '../../../platform/theme/common/styler.js';
  42. import { IThemeService } from '../../../platform/theme/common/themeService.js';
  43. import { ResizableHTMLElement } from './resizable.js';
  44. import { Context as SuggestContext } from './suggest.js';
  45. import { canExpandCompletionItem, SuggestDetailsOverlay, SuggestDetailsWidget } from './suggestWidgetDetails.js';
  46. import { getAriaId, ItemRenderer } from './suggestWidgetRenderer.js';
  47. /**
  48. * Suggest widget colors
  49. */
  50. export const editorSuggestWidgetBackground = registerColor('editorSuggestWidget.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.'));
  51. export const editorSuggestWidgetBorder = registerColor('editorSuggestWidget.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.'));
  52. export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', { dark: editorForeground, light: editorForeground, hc: editorForeground }, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.'));
  53. export const editorSuggestWidgetSelectedForeground = registerColor('editorSuggestWidget.selectedForeground', { dark: quickInputListFocusForeground, light: quickInputListFocusForeground, hc: quickInputListFocusForeground }, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.'));
  54. export const editorSuggestWidgetSelectedIconForeground = registerColor('editorSuggestWidget.selectedIconForeground', { dark: quickInputListFocusIconForeground, light: quickInputListFocusIconForeground, hc: quickInputListFocusIconForeground }, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.'));
  55. export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', { dark: quickInputListFocusBackground, light: quickInputListFocusBackground, hc: quickInputListFocusBackground }, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.'));
  56. export const editorSuggestWidgetHighlightForeground = registerColor('editorSuggestWidget.highlightForeground', { dark: listHighlightForeground, light: listHighlightForeground, hc: listHighlightForeground }, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.'));
  57. export const editorSuggestWidgetHighlightFocusForeground = registerColor('editorSuggestWidget.focusHighlightForeground', { dark: listFocusHighlightForeground, light: listFocusHighlightForeground, hc: listFocusHighlightForeground }, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.'));
  58. export const editorSuggestWidgetStatusForeground = registerColor('editorSuggestWidgetStatus.foreground', { dark: transparent(editorSuggestWidgetForeground, .5), light: transparent(editorSuggestWidgetForeground, .5), hc: transparent(editorSuggestWidgetForeground, .5) }, nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.'));
  59. class PersistedWidgetSize {
  60. constructor(_service, editor) {
  61. this._service = _service;
  62. this._key = `suggestWidget.size/${editor.getEditorType()}/${editor instanceof EmbeddedCodeEditorWidget}`;
  63. }
  64. restore() {
  65. var _a;
  66. const raw = (_a = this._service.get(this._key, 0 /* GLOBAL */)) !== null && _a !== void 0 ? _a : '';
  67. try {
  68. const obj = JSON.parse(raw);
  69. if (dom.Dimension.is(obj)) {
  70. return dom.Dimension.lift(obj);
  71. }
  72. }
  73. catch (_b) {
  74. // ignore
  75. }
  76. return undefined;
  77. }
  78. store(size) {
  79. this._service.store(this._key, JSON.stringify(size), 0 /* GLOBAL */, 1 /* MACHINE */);
  80. }
  81. reset() {
  82. this._service.remove(this._key, 0 /* GLOBAL */);
  83. }
  84. }
  85. let SuggestWidget = class SuggestWidget {
  86. constructor(editor, _storageService, _contextKeyService, _themeService, instantiationService) {
  87. this.editor = editor;
  88. this._storageService = _storageService;
  89. this._state = 0 /* Hidden */;
  90. this._isAuto = false;
  91. this._ignoreFocusEvents = false;
  92. this._forceRenderingAbove = false;
  93. this._explainMode = false;
  94. this._showTimeout = new TimeoutTimer();
  95. this._disposables = new DisposableStore();
  96. this._onDidSelect = new Emitter();
  97. this._onDidFocus = new Emitter();
  98. this._onDidHide = new Emitter();
  99. this._onDidShow = new Emitter();
  100. this.onDidSelect = this._onDidSelect.event;
  101. this.onDidFocus = this._onDidFocus.event;
  102. this.onDidHide = this._onDidHide.event;
  103. this.onDidShow = this._onDidShow.event;
  104. this._onDetailsKeydown = new Emitter();
  105. this.onDetailsKeyDown = this._onDetailsKeydown.event;
  106. this.element = new ResizableHTMLElement();
  107. this.element.domNode.classList.add('editor-widget', 'suggest-widget');
  108. this._contentWidget = new SuggestContentWidget(this, editor);
  109. this._persistedSize = new PersistedWidgetSize(_storageService, editor);
  110. class ResizeState {
  111. constructor(persistedSize, currentSize, persistHeight = false, persistWidth = false) {
  112. this.persistedSize = persistedSize;
  113. this.currentSize = currentSize;
  114. this.persistHeight = persistHeight;
  115. this.persistWidth = persistWidth;
  116. }
  117. }
  118. let state;
  119. this._disposables.add(this.element.onDidWillResize(() => {
  120. this._contentWidget.lockPreference();
  121. state = new ResizeState(this._persistedSize.restore(), this.element.size);
  122. }));
  123. this._disposables.add(this.element.onDidResize(e => {
  124. var _a, _b, _c, _d;
  125. this._resize(e.dimension.width, e.dimension.height);
  126. if (state) {
  127. state.persistHeight = state.persistHeight || !!e.north || !!e.south;
  128. state.persistWidth = state.persistWidth || !!e.east || !!e.west;
  129. }
  130. if (!e.done) {
  131. return;
  132. }
  133. if (state) {
  134. // only store width or height value that have changed and also
  135. // only store changes that are above a certain threshold
  136. const { itemHeight, defaultSize } = this.getLayoutInfo();
  137. const threshold = Math.round(itemHeight / 2);
  138. let { width, height } = this.element.size;
  139. if (!state.persistHeight || Math.abs(state.currentSize.height - height) <= threshold) {
  140. height = (_b = (_a = state.persistedSize) === null || _a === void 0 ? void 0 : _a.height) !== null && _b !== void 0 ? _b : defaultSize.height;
  141. }
  142. if (!state.persistWidth || Math.abs(state.currentSize.width - width) <= threshold) {
  143. width = (_d = (_c = state.persistedSize) === null || _c === void 0 ? void 0 : _c.width) !== null && _d !== void 0 ? _d : defaultSize.width;
  144. }
  145. this._persistedSize.store(new dom.Dimension(width, height));
  146. }
  147. // reset working state
  148. this._contentWidget.unlockPreference();
  149. state = undefined;
  150. }));
  151. this._messageElement = dom.append(this.element.domNode, dom.$('.message'));
  152. this._listElement = dom.append(this.element.domNode, dom.$('.tree'));
  153. const details = instantiationService.createInstance(SuggestDetailsWidget, this.editor);
  154. details.onDidClose(this.toggleDetails, this, this._disposables);
  155. this._details = new SuggestDetailsOverlay(details, this.editor);
  156. const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(105 /* suggest */).showIcons);
  157. applyIconStyle();
  158. const renderer = instantiationService.createInstance(ItemRenderer, this.editor);
  159. this._disposables.add(renderer);
  160. this._disposables.add(renderer.onDidToggleDetails(() => this.toggleDetails()));
  161. this._list = new List('SuggestWidget', this._listElement, {
  162. getHeight: (_element) => this.getLayoutInfo().itemHeight,
  163. getTemplateId: (_element) => 'suggestion'
  164. }, [renderer], {
  165. alwaysConsumeMouseWheel: true,
  166. useShadows: false,
  167. mouseSupport: false,
  168. accessibilityProvider: {
  169. getRole: () => 'option',
  170. getAriaLabel: (item) => {
  171. if (item.isResolved && this._isDetailsVisible()) {
  172. const { documentation, detail } = item.completion;
  173. const docs = strings.format('{0}{1}', detail || '', documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : '');
  174. return nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, docs: {1}", item.textLabel, docs);
  175. }
  176. else {
  177. return item.textLabel;
  178. }
  179. },
  180. getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"),
  181. getWidgetRole: () => 'listbox'
  182. }
  183. });
  184. this._status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode);
  185. const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(105 /* suggest */).showStatusBar);
  186. applyStatusBarStyle();
  187. this._disposables.add(attachListStyler(this._list, _themeService, {
  188. listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,
  189. listInactiveFocusOutline: activeContrastBorder
  190. }));
  191. this._disposables.add(_themeService.onDidColorThemeChange(t => this._onThemeChange(t)));
  192. this._onThemeChange(_themeService.getColorTheme());
  193. this._disposables.add(this._list.onMouseDown(e => this._onListMouseDownOrTap(e)));
  194. this._disposables.add(this._list.onTap(e => this._onListMouseDownOrTap(e)));
  195. this._disposables.add(this._list.onDidChangeSelection(e => this._onListSelection(e)));
  196. this._disposables.add(this._list.onDidChangeFocus(e => this._onListFocus(e)));
  197. this._disposables.add(this.editor.onDidChangeCursorSelection(() => this._onCursorSelectionChanged()));
  198. this._disposables.add(this.editor.onDidChangeConfiguration(e => {
  199. if (e.hasChanged(105 /* suggest */)) {
  200. applyStatusBarStyle();
  201. applyIconStyle();
  202. }
  203. }));
  204. this._ctxSuggestWidgetVisible = SuggestContext.Visible.bindTo(_contextKeyService);
  205. this._ctxSuggestWidgetDetailsVisible = SuggestContext.DetailsVisible.bindTo(_contextKeyService);
  206. this._ctxSuggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(_contextKeyService);
  207. this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => {
  208. this._onDetailsKeydown.fire(e);
  209. }));
  210. this._disposables.add(this.editor.onMouseDown((e) => this._onEditorMouseDown(e)));
  211. }
  212. dispose() {
  213. var _a;
  214. this._details.widget.dispose();
  215. this._details.dispose();
  216. this._list.dispose();
  217. this._status.dispose();
  218. this._disposables.dispose();
  219. (_a = this._loadingTimeout) === null || _a === void 0 ? void 0 : _a.dispose();
  220. this._showTimeout.dispose();
  221. this._contentWidget.dispose();
  222. this.element.dispose();
  223. }
  224. _onEditorMouseDown(mouseEvent) {
  225. if (this._details.widget.domNode.contains(mouseEvent.target.element)) {
  226. // Clicking inside details
  227. this._details.widget.domNode.focus();
  228. }
  229. else {
  230. // Clicking outside details and inside suggest
  231. if (this.element.domNode.contains(mouseEvent.target.element)) {
  232. this.editor.focus();
  233. }
  234. }
  235. }
  236. _onCursorSelectionChanged() {
  237. if (this._state !== 0 /* Hidden */) {
  238. this._contentWidget.layout();
  239. }
  240. }
  241. _onListMouseDownOrTap(e) {
  242. if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {
  243. return;
  244. }
  245. // prevent stealing browser focus from the editor
  246. e.browserEvent.preventDefault();
  247. e.browserEvent.stopPropagation();
  248. this._select(e.element, e.index);
  249. }
  250. _onListSelection(e) {
  251. if (e.elements.length) {
  252. this._select(e.elements[0], e.indexes[0]);
  253. }
  254. }
  255. _select(item, index) {
  256. const completionModel = this._completionModel;
  257. if (completionModel) {
  258. this._onDidSelect.fire({ item, index, model: completionModel });
  259. this.editor.focus();
  260. }
  261. }
  262. _onThemeChange(theme) {
  263. this._details.widget.borderWidth = theme.type === 'hc' ? 2 : 1;
  264. }
  265. _onListFocus(e) {
  266. var _a;
  267. if (this._ignoreFocusEvents) {
  268. return;
  269. }
  270. if (!e.elements.length) {
  271. if (this._currentSuggestionDetails) {
  272. this._currentSuggestionDetails.cancel();
  273. this._currentSuggestionDetails = undefined;
  274. this._focusedItem = undefined;
  275. }
  276. this.editor.setAriaOptions({ activeDescendant: undefined });
  277. return;
  278. }
  279. if (!this._completionModel) {
  280. return;
  281. }
  282. const item = e.elements[0];
  283. const index = e.indexes[0];
  284. if (item !== this._focusedItem) {
  285. (_a = this._currentSuggestionDetails) === null || _a === void 0 ? void 0 : _a.cancel();
  286. this._currentSuggestionDetails = undefined;
  287. this._focusedItem = item;
  288. this._list.reveal(index);
  289. this._currentSuggestionDetails = createCancelablePromise((token) => __awaiter(this, void 0, void 0, function* () {
  290. const loading = disposableTimeout(() => {
  291. if (this._isDetailsVisible()) {
  292. this.showDetails(true);
  293. }
  294. }, 250);
  295. token.onCancellationRequested(() => loading.dispose());
  296. const result = yield item.resolve(token);
  297. loading.dispose();
  298. return result;
  299. }));
  300. this._currentSuggestionDetails.then(() => {
  301. if (index >= this._list.length || item !== this._list.element(index)) {
  302. return;
  303. }
  304. // item can have extra information, so re-render
  305. this._ignoreFocusEvents = true;
  306. this._list.splice(index, 1, [item]);
  307. this._list.setFocus([index]);
  308. this._ignoreFocusEvents = false;
  309. if (this._isDetailsVisible()) {
  310. this.showDetails(false);
  311. }
  312. else {
  313. this.element.domNode.classList.remove('docs-side');
  314. }
  315. this.editor.setAriaOptions({ activeDescendant: getAriaId(index) });
  316. }).catch(onUnexpectedError);
  317. }
  318. // emit an event
  319. this._onDidFocus.fire({ item, index, model: this._completionModel });
  320. }
  321. _setState(state) {
  322. if (this._state === state) {
  323. return;
  324. }
  325. this._state = state;
  326. this.element.domNode.classList.toggle('frozen', state === 4 /* Frozen */);
  327. this.element.domNode.classList.remove('message');
  328. switch (state) {
  329. case 0 /* Hidden */:
  330. dom.hide(this._messageElement, this._listElement, this._status.element);
  331. this._details.hide(true);
  332. this._status.hide();
  333. this._contentWidget.hide();
  334. this._ctxSuggestWidgetVisible.reset();
  335. this._ctxSuggestWidgetMultipleSuggestions.reset();
  336. this._showTimeout.cancel();
  337. this.element.domNode.classList.remove('visible');
  338. this._list.splice(0, this._list.length);
  339. this._focusedItem = undefined;
  340. this._cappedHeight = undefined;
  341. this._explainMode = false;
  342. break;
  343. case 1 /* Loading */:
  344. this.element.domNode.classList.add('message');
  345. this._messageElement.textContent = SuggestWidget.LOADING_MESSAGE;
  346. dom.hide(this._listElement, this._status.element);
  347. dom.show(this._messageElement);
  348. this._details.hide();
  349. this._show();
  350. this._focusedItem = undefined;
  351. break;
  352. case 2 /* Empty */:
  353. this.element.domNode.classList.add('message');
  354. this._messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;
  355. dom.hide(this._listElement, this._status.element);
  356. dom.show(this._messageElement);
  357. this._details.hide();
  358. this._show();
  359. this._focusedItem = undefined;
  360. break;
  361. case 3 /* Open */:
  362. dom.hide(this._messageElement);
  363. dom.show(this._listElement, this._status.element);
  364. this._show();
  365. break;
  366. case 4 /* Frozen */:
  367. dom.hide(this._messageElement);
  368. dom.show(this._listElement, this._status.element);
  369. this._show();
  370. break;
  371. case 5 /* Details */:
  372. dom.hide(this._messageElement);
  373. dom.show(this._listElement, this._status.element);
  374. this._details.show();
  375. this._show();
  376. break;
  377. }
  378. }
  379. _show() {
  380. this._status.show();
  381. this._contentWidget.show();
  382. this._layout(this._persistedSize.restore());
  383. this._ctxSuggestWidgetVisible.set(true);
  384. this._showTimeout.cancelAndSet(() => {
  385. this.element.domNode.classList.add('visible');
  386. this._onDidShow.fire(this);
  387. }, 100);
  388. }
  389. showTriggered(auto, delay) {
  390. if (this._state !== 0 /* Hidden */) {
  391. return;
  392. }
  393. this._contentWidget.setPosition(this.editor.getPosition());
  394. this._isAuto = !!auto;
  395. if (!this._isAuto) {
  396. this._loadingTimeout = disposableTimeout(() => this._setState(1 /* Loading */), delay);
  397. }
  398. }
  399. showSuggestions(completionModel, selectionIndex, isFrozen, isAuto) {
  400. var _a, _b;
  401. this._contentWidget.setPosition(this.editor.getPosition());
  402. (_a = this._loadingTimeout) === null || _a === void 0 ? void 0 : _a.dispose();
  403. (_b = this._currentSuggestionDetails) === null || _b === void 0 ? void 0 : _b.cancel();
  404. this._currentSuggestionDetails = undefined;
  405. if (this._completionModel !== completionModel) {
  406. this._completionModel = completionModel;
  407. }
  408. if (isFrozen && this._state !== 2 /* Empty */ && this._state !== 0 /* Hidden */) {
  409. this._setState(4 /* Frozen */);
  410. return;
  411. }
  412. const visibleCount = this._completionModel.items.length;
  413. const isEmpty = visibleCount === 0;
  414. this._ctxSuggestWidgetMultipleSuggestions.set(visibleCount > 1);
  415. if (isEmpty) {
  416. this._setState(isAuto ? 0 /* Hidden */ : 2 /* Empty */);
  417. this._completionModel = undefined;
  418. return;
  419. }
  420. this._focusedItem = undefined;
  421. this._list.splice(0, this._list.length, this._completionModel.items);
  422. this._setState(isFrozen ? 4 /* Frozen */ : 3 /* Open */);
  423. this._list.reveal(selectionIndex, 0);
  424. this._list.setFocus([selectionIndex]);
  425. this._layout(this.element.size);
  426. // Reset focus border
  427. this._details.widget.domNode.classList.remove('focused');
  428. }
  429. selectNextPage() {
  430. switch (this._state) {
  431. case 0 /* Hidden */:
  432. return false;
  433. case 5 /* Details */:
  434. this._details.widget.pageDown();
  435. return true;
  436. case 1 /* Loading */:
  437. return !this._isAuto;
  438. default:
  439. this._list.focusNextPage();
  440. return true;
  441. }
  442. }
  443. selectNext() {
  444. switch (this._state) {
  445. case 0 /* Hidden */:
  446. return false;
  447. case 1 /* Loading */:
  448. return !this._isAuto;
  449. default:
  450. this._list.focusNext(1, true);
  451. return true;
  452. }
  453. }
  454. selectLast() {
  455. switch (this._state) {
  456. case 0 /* Hidden */:
  457. return false;
  458. case 5 /* Details */:
  459. this._details.widget.scrollBottom();
  460. return true;
  461. case 1 /* Loading */:
  462. return !this._isAuto;
  463. default:
  464. this._list.focusLast();
  465. return true;
  466. }
  467. }
  468. selectPreviousPage() {
  469. switch (this._state) {
  470. case 0 /* Hidden */:
  471. return false;
  472. case 5 /* Details */:
  473. this._details.widget.pageUp();
  474. return true;
  475. case 1 /* Loading */:
  476. return !this._isAuto;
  477. default:
  478. this._list.focusPreviousPage();
  479. return true;
  480. }
  481. }
  482. selectPrevious() {
  483. switch (this._state) {
  484. case 0 /* Hidden */:
  485. return false;
  486. case 1 /* Loading */:
  487. return !this._isAuto;
  488. default:
  489. this._list.focusPrevious(1, true);
  490. return false;
  491. }
  492. }
  493. selectFirst() {
  494. switch (this._state) {
  495. case 0 /* Hidden */:
  496. return false;
  497. case 5 /* Details */:
  498. this._details.widget.scrollTop();
  499. return true;
  500. case 1 /* Loading */:
  501. return !this._isAuto;
  502. default:
  503. this._list.focusFirst();
  504. return true;
  505. }
  506. }
  507. getFocusedItem() {
  508. if (this._state !== 0 /* Hidden */
  509. && this._state !== 2 /* Empty */
  510. && this._state !== 1 /* Loading */
  511. && this._completionModel) {
  512. return {
  513. item: this._list.getFocusedElements()[0],
  514. index: this._list.getFocus()[0],
  515. model: this._completionModel
  516. };
  517. }
  518. return undefined;
  519. }
  520. toggleDetailsFocus() {
  521. if (this._state === 5 /* Details */) {
  522. this._setState(3 /* Open */);
  523. this._details.widget.domNode.classList.remove('focused');
  524. }
  525. else if (this._state === 3 /* Open */ && this._isDetailsVisible()) {
  526. this._setState(5 /* Details */);
  527. this._details.widget.domNode.classList.add('focused');
  528. }
  529. }
  530. toggleDetails() {
  531. if (this._isDetailsVisible()) {
  532. // hide details widget
  533. this._ctxSuggestWidgetDetailsVisible.set(false);
  534. this._setDetailsVisible(false);
  535. this._details.hide();
  536. this.element.domNode.classList.remove('shows-details');
  537. }
  538. else if ((canExpandCompletionItem(this._list.getFocusedElements()[0]) || this._explainMode) && (this._state === 3 /* Open */ || this._state === 5 /* Details */ || this._state === 4 /* Frozen */)) {
  539. // show details widget (iff possible)
  540. this._ctxSuggestWidgetDetailsVisible.set(true);
  541. this._setDetailsVisible(true);
  542. this.showDetails(false);
  543. }
  544. }
  545. showDetails(loading) {
  546. this._details.show();
  547. if (loading) {
  548. this._details.widget.renderLoading();
  549. }
  550. else {
  551. this._details.widget.renderItem(this._list.getFocusedElements()[0], this._explainMode);
  552. }
  553. this._positionDetails();
  554. this.editor.focus();
  555. this.element.domNode.classList.add('shows-details');
  556. }
  557. toggleExplainMode() {
  558. if (this._list.getFocusedElements()[0]) {
  559. this._explainMode = !this._explainMode;
  560. if (!this._isDetailsVisible()) {
  561. this.toggleDetails();
  562. }
  563. else {
  564. this.showDetails(false);
  565. }
  566. }
  567. }
  568. resetPersistedSize() {
  569. this._persistedSize.reset();
  570. }
  571. hideWidget() {
  572. var _a;
  573. (_a = this._loadingTimeout) === null || _a === void 0 ? void 0 : _a.dispose();
  574. this._setState(0 /* Hidden */);
  575. this._onDidHide.fire(this);
  576. this.element.clearSashHoverState();
  577. // ensure that a reasonable widget height is persisted so that
  578. // accidential "resize-to-single-items" cases aren't happening
  579. const dim = this._persistedSize.restore();
  580. const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3);
  581. if (dim && dim.height < minPersistedHeight) {
  582. this._persistedSize.store(dim.with(undefined, minPersistedHeight));
  583. }
  584. }
  585. isFrozen() {
  586. return this._state === 4 /* Frozen */;
  587. }
  588. _afterRender(position) {
  589. if (position === null) {
  590. if (this._isDetailsVisible()) {
  591. this._details.hide(); //todo@jrieken soft-hide
  592. }
  593. return;
  594. }
  595. if (this._state === 2 /* Empty */ || this._state === 1 /* Loading */) {
  596. // no special positioning when widget isn't showing list
  597. return;
  598. }
  599. if (this._isDetailsVisible()) {
  600. this._details.show();
  601. }
  602. this._positionDetails();
  603. }
  604. _layout(size) {
  605. var _a, _b, _c;
  606. if (!this.editor.hasModel()) {
  607. return;
  608. }
  609. if (!this.editor.getDomNode()) {
  610. // happens when running tests
  611. return;
  612. }
  613. const bodyBox = dom.getClientArea(document.body);
  614. const info = this.getLayoutInfo();
  615. if (!size) {
  616. size = info.defaultSize;
  617. }
  618. let height = size.height;
  619. let width = size.width;
  620. // status bar
  621. this._status.element.style.lineHeight = `${info.itemHeight}px`;
  622. if (this._state === 2 /* Empty */ || this._state === 1 /* Loading */) {
  623. // showing a message only
  624. height = info.itemHeight + info.borderHeight;
  625. width = info.defaultSize.width / 2;
  626. this.element.enableSashes(false, false, false, false);
  627. this.element.minSize = this.element.maxSize = new dom.Dimension(width, height);
  628. this._contentWidget.setPreference(2 /* BELOW */);
  629. }
  630. else {
  631. // showing items
  632. // width math
  633. const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding;
  634. if (width > maxWidth) {
  635. width = maxWidth;
  636. }
  637. const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width;
  638. // height math
  639. const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight;
  640. const minHeight = info.itemHeight + info.statusBarHeight;
  641. const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode());
  642. const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition());
  643. const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height;
  644. const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight);
  645. const availableSpaceAbove = editorBox.top + cursorBox.top - info.verticalPadding;
  646. const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight);
  647. let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight);
  648. if (height === ((_a = this._cappedHeight) === null || _a === void 0 ? void 0 : _a.capped)) {
  649. // Restore the old (wanted) height when the current
  650. // height is capped to fit
  651. height = this._cappedHeight.wanted;
  652. }
  653. if (height < minHeight) {
  654. height = minHeight;
  655. }
  656. if (height > maxHeight) {
  657. height = maxHeight;
  658. }
  659. const forceRenderingAboveRequiredSpace = 150;
  660. if (height > maxHeightBelow || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) {
  661. this._contentWidget.setPreference(1 /* ABOVE */);
  662. this.element.enableSashes(true, true, false, false);
  663. maxHeight = maxHeightAbove;
  664. }
  665. else {
  666. this._contentWidget.setPreference(2 /* BELOW */);
  667. this.element.enableSashes(false, true, true, false);
  668. maxHeight = maxHeightBelow;
  669. }
  670. this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height);
  671. this.element.maxSize = new dom.Dimension(maxWidth, maxHeight);
  672. this.element.minSize = new dom.Dimension(220, minHeight);
  673. // Know when the height was capped to fit and remember
  674. // the wanted height for later. This is required when going
  675. // left to widen suggestions.
  676. this._cappedHeight = height === fullHeight
  677. ? { wanted: (_c = (_b = this._cappedHeight) === null || _b === void 0 ? void 0 : _b.wanted) !== null && _c !== void 0 ? _c : size.height, capped: height }
  678. : undefined;
  679. }
  680. this._resize(width, height);
  681. }
  682. _resize(width, height) {
  683. const { width: maxWidth, height: maxHeight } = this.element.maxSize;
  684. width = Math.min(maxWidth, width);
  685. height = Math.min(maxHeight, height);
  686. const { statusBarHeight } = this.getLayoutInfo();
  687. this._list.layout(height - statusBarHeight, width);
  688. this._listElement.style.height = `${height - statusBarHeight}px`;
  689. this.element.layout(height, width);
  690. this._contentWidget.layout();
  691. this._positionDetails();
  692. }
  693. _positionDetails() {
  694. var _a;
  695. if (this._isDetailsVisible()) {
  696. this._details.placeAtAnchor(this.element.domNode, ((_a = this._contentWidget.getPosition()) === null || _a === void 0 ? void 0 : _a.preference[0]) === 2 /* BELOW */);
  697. }
  698. }
  699. getLayoutInfo() {
  700. const fontInfo = this.editor.getOption(43 /* fontInfo */);
  701. const itemHeight = clamp(this.editor.getOption(107 /* suggestLineHeight */) || fontInfo.lineHeight, 8, 1000);
  702. const statusBarHeight = !this.editor.getOption(105 /* suggest */).showStatusBar || this._state === 2 /* Empty */ || this._state === 1 /* Loading */ ? 0 : itemHeight;
  703. const borderWidth = this._details.widget.borderWidth;
  704. const borderHeight = 2 * borderWidth;
  705. return {
  706. itemHeight,
  707. statusBarHeight,
  708. borderWidth,
  709. borderHeight,
  710. typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth,
  711. verticalPadding: 22,
  712. horizontalPadding: 14,
  713. defaultSize: new dom.Dimension(430, statusBarHeight + 12 * itemHeight + borderHeight)
  714. };
  715. }
  716. _isDetailsVisible() {
  717. return this._storageService.getBoolean('expandSuggestionDocs', 0 /* GLOBAL */, false);
  718. }
  719. _setDetailsVisible(value) {
  720. this._storageService.store('expandSuggestionDocs', value, 0 /* GLOBAL */, 0 /* USER */);
  721. }
  722. forceRenderingAbove() {
  723. if (!this._forceRenderingAbove) {
  724. this._forceRenderingAbove = true;
  725. this._layout(this._persistedSize.restore());
  726. }
  727. }
  728. stopForceRenderingAbove() {
  729. this._forceRenderingAbove = false;
  730. }
  731. };
  732. SuggestWidget.LOADING_MESSAGE = nls.localize('suggestWidget.loading', "Loading...");
  733. SuggestWidget.NO_SUGGESTIONS_MESSAGE = nls.localize('suggestWidget.noSuggestions', "No suggestions.");
  734. SuggestWidget = __decorate([
  735. __param(1, IStorageService),
  736. __param(2, IContextKeyService),
  737. __param(3, IThemeService),
  738. __param(4, IInstantiationService)
  739. ], SuggestWidget);
  740. export { SuggestWidget };
  741. export class SuggestContentWidget {
  742. constructor(_widget, _editor) {
  743. this._widget = _widget;
  744. this._editor = _editor;
  745. this.allowEditorOverflow = true;
  746. this.suppressMouseDown = false;
  747. this._preferenceLocked = false;
  748. this._added = false;
  749. this._hidden = false;
  750. }
  751. dispose() {
  752. if (this._added) {
  753. this._added = false;
  754. this._editor.removeContentWidget(this);
  755. }
  756. }
  757. getId() {
  758. return 'editor.widget.suggestWidget';
  759. }
  760. getDomNode() {
  761. return this._widget.element.domNode;
  762. }
  763. show() {
  764. this._hidden = false;
  765. if (!this._added) {
  766. this._added = true;
  767. this._editor.addContentWidget(this);
  768. }
  769. }
  770. hide() {
  771. if (!this._hidden) {
  772. this._hidden = true;
  773. this.layout();
  774. }
  775. }
  776. layout() {
  777. this._editor.layoutContentWidget(this);
  778. }
  779. getPosition() {
  780. if (this._hidden || !this._position || !this._preference) {
  781. return null;
  782. }
  783. return {
  784. position: this._position,
  785. preference: [this._preference]
  786. };
  787. }
  788. beforeRender() {
  789. const { height, width } = this._widget.element.size;
  790. const { borderWidth, horizontalPadding } = this._widget.getLayoutInfo();
  791. return new dom.Dimension(width + 2 * borderWidth + horizontalPadding, height + 2 * borderWidth);
  792. }
  793. afterRender(position) {
  794. this._widget._afterRender(position);
  795. }
  796. setPreference(preference) {
  797. if (!this._preferenceLocked) {
  798. this._preference = preference;
  799. }
  800. }
  801. lockPreference() {
  802. this._preferenceLocked = true;
  803. }
  804. unlockPreference() {
  805. this._preferenceLocked = false;
  806. }
  807. setPosition(position) {
  808. this._position = position;
  809. }
  810. }