diff --git a/src/vs/editor/contrib/suggest/browser/media/suggest.css b/src/vs/editor/contrib/suggest/browser/media/suggest.css index 3714be6e58fad2ea8f75bae9a9701b29ba4e38fd..4b8e1fca7e642cea3180932b5cca1267d4edd950 100644 --- a/src/vs/editor/contrib/suggest/browser/media/suggest.css +++ b/src/vs/editor/contrib/suggest/browser/media/suggest.css @@ -6,7 +6,7 @@ /* Suggest */ .monaco-editor .suggest-widget { z-index: 40; - width: 438px; + width: 660px; } .monaco-editor .suggest-widget.visible { @@ -23,7 +23,17 @@ .monaco-editor .suggest-widget > .tree { height: 100%; - width: 100%; + width: 220px; + float: left; + box-sizing: border-box; +} + +.monaco-editor .suggest-widget.list-right > .tree { + float: right +} + +.monaco-editor .suggest-widget.docs-below > .tree { + float: none; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row { @@ -49,71 +59,10 @@ text-overflow: ellipsis; } -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs { - display: none; - overflow: hidden; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs > .docs-text { - flex: 2; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - opacity: 0.85; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs > .docs-text.no-docs { - opacity: 0.5; - font-style: italic; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs > .docs-details { - opacity: 0.6; - background-image: url('./info.svg'); - background-position: center center; - background-repeat: no-repeat; - background-size: 70%; -} - -.monaco-editor .suggest-widget .details > .header > .go-back, -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs > .docs-details { - color: #0035DD; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .docs > .docs-details:hover { - opacity: 1; -} - .monaco-editor .suggest-widget:not(.frozen) .monaco-highlighted-label .highlight { font-weight: bold; } -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .type-label { - display: none; - margin-left: 0.8em; - flex: 1; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.7; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .type-label > .monaco-tokenized-source { - display: inline; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused > .contents > .main > .type-label { - display: inline; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .type-label { - display: inline; -} - -.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused .docs { - display: flex; -} - .monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon { display: block; height: 16px; @@ -160,37 +109,16 @@ } .monaco-editor .suggest-widget .details { - height: 100%; - box-sizing: border-box; + max-height: 216px; display: flex; flex-direction: column; cursor: default; -} - -.monaco-editor .suggest-widget .details > .header { - padding: 4px 5px; - display: flex; box-sizing: border-box; - border-bottom: 1px solid rgba(204, 204, 204, 0.5); + width: 440px; } -.monaco-editor .suggest-widget .details > .header > .title { - flex: 2; - overflow: hidden; - text-overflow: ellipsis; -} - -.monaco-editor .suggest-widget .details > .header > .go-back { - cursor: pointer; - opacity: 0.6; - background-image: url('./back.svg'); - background-size: 70%; - background-position: center center; - background-repeat: no-repeat; -} - -.monaco-editor .suggest-widget .details > .header > .go-back:hover { - opacity: 1; +.monaco-editor .suggest-widget .details.no-docs { + display: none; } .monaco-editor .suggest-widget .details > .monaco-scrollable-element { @@ -218,30 +146,12 @@ display: none; } -/* Dark theme */ - -.monaco-editor.vs-dark .suggest-widget .details > .header { - border-color: rgba(85,85,85,0.5); -} - -.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .docs > .docs-details, -.monaco-editor.vs-dark .suggest-widget .details > .header > .go-back { - color: #4E94CE; -} - - /* High Contrast Theming */ -.monaco-editor.hc-black .suggest-widget .details > .monaco-scrollable-element > .body > .docs, -.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .docs { +.monaco-editor.hc-black .suggest-widget .details > .monaco-scrollable-element > .body > .docs { color: #C07A7A; } -.monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .docs > .docs-details, -.monaco-editor.hc-black .suggest-widget .details > .header > .go-back { - color: #4E94CE; -} - .monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row .icon, .monaco-editor.hc-black .suggest-widget .monaco-list .monaco-list-row .icon { background-image: url('Misc_inverse_16x.svg'); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 09720eae57b209ecfc5403d2ee645710ca449598..e19aad6d8ea93ec250c243ffbbd4c158c3af67f3 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -222,7 +222,7 @@ export class SuggestController implements IEditorContribution { cancelSuggestWidget(): void { if (this._widget) { this._model.cancel(); - this._widget.hideDetailsOrHideWidget(); + this._widget.hideWidget(); } } @@ -264,7 +264,7 @@ export class SuggestController implements IEditorContribution { toggleSuggestionDetails(): void { if (this._widget) { - this._widget.toggleDetails(); + this._widget.toggleDetailsFocus(); } } } diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 27a980495d802b728b24644e031826087f31e165..09ac145137cac2ffb2ba5646b1ba7348cdf50db6 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -30,6 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, editorWidgetBackground, contrastBorder, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Position } from 'vs/editor/common/core/position'; const sticky = false; // for development purposes @@ -38,9 +39,6 @@ interface ISuggestionTemplateData { icon: HTMLElement; colorspan: HTMLElement; highlightedLabel: HighlightedLabel; - typeLabel: HTMLElement; - documentationDetails: HTMLElement; - documentation: HTMLElement; disposables: IDisposable[]; } @@ -64,7 +62,7 @@ function canExpandCompletionItem(item: ICompletionItem) { if (suggestion.documentation) { return true; } - return (suggestion.detail || '').indexOf('\n') >= 0; + return (suggestion.detail && suggestion.detail !== suggestion.label); } class Renderer implements IRenderer { @@ -96,13 +94,6 @@ class Renderer implements IRenderer { const main = append(text, $('.main')); data.highlightedLabel = new HighlightedLabel(main); data.disposables.push(data.highlightedLabel); - data.typeLabel = append(main, $('span.type-label')); - - const docs = append(text, $('.docs')); - data.documentation = append(docs, $('span.docs-text')); - data.documentationDetails = append(docs, $('span.docs-details')); - data.documentationDetails.title = nls.localize('readMore', "Read More...{0}", this.triggerKeybindingLabel); - const configureFont = () => { const configuration = this.editor.getConfiguration(); const fontFamily = configuration.fontInfo.fontFamily; @@ -116,8 +107,6 @@ class Renderer implements IRenderer { main.style.lineHeight = lineHeightPx; data.icon.style.height = lineHeightPx; data.icon.style.width = lineHeightPx; - data.documentationDetails.style.height = lineHeightPx; - data.documentationDetails.style.width = lineHeightPx; }; configureFont(); @@ -151,26 +140,8 @@ class Renderer implements IRenderer { } data.highlightedLabel.set(suggestion.label, createMatches(element.matches)); - data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, ''); - data.documentation.textContent = suggestion.documentation || ''; - if (canExpandCompletionItem(element)) { - show(data.documentationDetails); - data.documentationDetails.onmousedown = e => { - e.stopPropagation(); - e.preventDefault(); - }; - data.documentationDetails.onclick = e => { - e.stopPropagation(); - e.preventDefault(); - this.widget.toggleDetails(); - }; - } else { - hide(data.documentationDetails); - data.documentationDetails.onmousedown = null; - data.documentationDetails.onclick = null; - } } disposeTemplate(templateData: ISuggestionTemplateData): void { @@ -191,9 +162,6 @@ const enum State { class SuggestionDetails { private el: HTMLElement; - private title: HTMLElement; - private titleLabel: HighlightedLabel; - private back: HTMLElement; private scrollbar: DomScrollableElement; private body: HTMLElement; private type: HTMLElement; @@ -211,13 +179,6 @@ class SuggestionDetails { this.el = append(container, $('.details')); this.disposables.push(toDisposable(() => container.removeChild(this.el))); - const header = append(this.el, $('.header')); - this.title = append(header, $('span.title')); - this.titleLabel = new HighlightedLabel(this.title); - this.disposables.push(this.titleLabel); - - this.back = append(header, $('span.go-back')); - this.back.title = nls.localize('goback', "Go back"); this.body = $('.body'); this.scrollbar = new DomScrollableElement(this.body, { canUseTranslate3d: false }); @@ -240,26 +201,18 @@ class SuggestionDetails { } render(item: ICompletionItem): void { - if (!item) { - this.titleLabel.set(''); + if (!item || !canExpandCompletionItem(item)) { this.type.textContent = ''; this.docs.textContent = ''; + addClass(this.el, 'no-docs'); this.ariaLabel = null; return; } - - this.titleLabel.set(item.suggestion.label, createMatches(item.matches)); + removeClass(this.el, 'no-docs'); this.type.innerText = item.suggestion.detail || ''; this.docs.textContent = item.suggestion.documentation; - this.back.onmousedown = e => { - e.preventDefault(); - e.stopPropagation(); - }; - this.back.onclick = e => { - e.preventDefault(); - e.stopPropagation(); - this.widget.toggleDetails(); - }; + + this.el.style.height = this.type.clientHeight + this.docs.clientHeight + 'px'; this.body.scrollTop = 0; this.scrollbar.scanDomNode(); @@ -299,15 +252,10 @@ class SuggestionDetails { const configuration = this.editor.getConfiguration(); const fontFamily = configuration.fontInfo.fontFamily; const fontSize = configuration.contribInfo.suggestFontSize || configuration.fontInfo.fontSize; - const lineHeight = configuration.contribInfo.suggestLineHeight || configuration.fontInfo.lineHeight; const fontSizePx = `${fontSize}px`; - const lineHeightPx = `${lineHeight}px`; this.el.style.fontSize = fontSizePx; - this.title.style.fontFamily = fontFamily; this.type.style.fontFamily = fontFamily; - this.back.style.height = lineHeightPx; - this.back.style.width = lineHeightPx; } dispose(): void { @@ -359,6 +307,14 @@ export class SuggestWidget implements IContentWidget, IDelegate readonly onDidHide: Event = this.onDidHideEmitter.event; readonly onDidShow: Event = this.onDidShowEmitter.event; + private preferredPosition: Position; + private readonly minWidgetWidth = 440; + private readonly maxWidgetWidth = 660; + private readonly listWidth = 220; + private readonly minDocsWidth = 220; + private readonly maxDocsWidth = 440; + private readonly widgetWidthInSmallestEditor = 300; + constructor( private editor: ICodeEditor, @ITelemetryService private telemetryService: ITelemetryService, @@ -477,12 +433,14 @@ export class SuggestWidget implements IContentWidget, IDelegate private onThemeChange(theme: ITheme) { let backgroundColor = theme.getColor(editorSuggestWidgetBackground); if (backgroundColor) { - this.element.style.backgroundColor = backgroundColor.toString(); + this.listElement.style.backgroundColor = backgroundColor.toString(); + this.details.element.style.backgroundColor = backgroundColor.toString(); } let borderColor = theme.getColor(editorSuggestWidgetBorder); if (borderColor) { let borderWidth = theme.type === 'hc' ? 2 : 1; - this.element.style.border = `${borderWidth}px solid ${borderColor}`; + this.listElement.style.border = `${borderWidth}px solid ${borderColor}`; + this.details.element.style.border = `${borderWidth}px solid ${borderColor}`; } } @@ -550,7 +508,7 @@ export class SuggestWidget implements IContentWidget, IDelegate this.list.setFocus([index]); this.updateWidgetHeight(); this.list.reveal(index); - + this.showDetails(); this._ariaAlert(this._getSuggestionAriaAlertLabel(item)); }) .then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err)) @@ -592,8 +550,8 @@ export class SuggestWidget implements IContentWidget, IDelegate this.show(); break; case State.Open: - hide(this.messageElement, this.details.element); - show(this.listElement); + hide(this.messageElement); + show(this.listElement, this.details.element); this.show(); break; case State.Frozen: @@ -602,8 +560,8 @@ export class SuggestWidget implements IContentWidget, IDelegate this.show(); break; case State.Details: - hide(this.messageElement, this.listElement); - show(this.details.element); + hide(this.messageElement); + show(this.details.element, this.listElement); this.show(); this._ariaAlert(this.details.getAriaLabel()); break; @@ -777,26 +735,21 @@ export class SuggestWidget implements IContentWidget, IDelegate return undefined; } - toggleDetails(): void { + toggleDetailsFocus(): void { if (this.state === State.Details) { this.setState(State.Open); - this.editor.focus(); - return; + } else if (this.state === State.Open) { + this.setState(State.Details); } + } - if (this.state !== State.Open) { - return; - } - - const item = this.list.getFocusedElements()[0]; - - if (!item || !canExpandCompletionItem(item)) { + showDetails(): void { + if (this.state !== State.Open && this.state !== State.Details) { return; } - this.setState(State.Details); + this.show(); this.editor.focus(); - this.telemetryService.publicLog('suggestWidget:toggleDetails', this.editor.getTelemetryData()); } private show(): void { @@ -821,21 +774,13 @@ export class SuggestWidget implements IContentWidget, IDelegate this.onDidHideEmitter.fire(this); } - hideDetailsOrHideWidget(): void { - if (this.state === State.Details) { - this.toggleDetails(); - } else { - this.hideWidget(); - } - } - getPosition(): IContentWidgetPosition { if (this.state === State.Hidden) { return null; } return { - position: this.editor.getPosition(), + position: this.preferredPosition ? this.preferredPosition : this.editor.getPosition(), preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] }; } @@ -850,33 +795,118 @@ export class SuggestWidget implements IContentWidget, IDelegate private updateWidgetHeight(): number { let height = 0; + let maxSuggestionsToShow = this.editor.getLayoutInfo().contentWidth > this.minWidgetWidth ? 11 : 5; if (this.state === State.Empty || this.state === State.Loading) { height = this.unfocusedHeight; - } else if (this.state === State.Details) { - height = 12 * this.unfocusedHeight; } else { const focus = this.list.getFocusedElements()[0]; const focusHeight = focus ? this.getHeight(focus) : this.unfocusedHeight; height = focusHeight; const suggestionCount = (this.list.contentHeight - focusHeight) / this.unfocusedHeight; - height += Math.min(suggestionCount, 11) * this.unfocusedHeight; + height += Math.min(suggestionCount, maxSuggestionsToShow) * this.unfocusedHeight; } this.element.style.lineHeight = `${this.unfocusedHeight}px`; - this.element.style.height = `${height}px`; + this.listElement.style.height = `${height}px`; this.list.layout(height); + + this.adjustWidgetWidth(); this.editor.layoutContentWidget(this); return height; } + private adjustWidgetWidth() { + const perColumnWidth = this.editor.getLayoutInfo().contentWidth / this.editor.getLayoutInfo().viewportColumn; + const spaceOntheLeft = Math.floor(this.editor.getPosition().column * perColumnWidth) - this.editor.getScrollLeft(); + const spaceOntheRight = this.editor.getLayoutInfo().contentWidth - spaceOntheLeft; + const scrolledColumns = Math.floor(this.editor.getScrollLeft() / perColumnWidth); + + // Reset + this.preferredPosition = this.editor.getPosition(); + this.details.element.style.width = `${this.maxDocsWidth}px`; + this.element.style.width = `${this.maxWidgetWidth}px`; + this.listElement.style.width = `${this.listWidth}px`; + removeClass(this.element, 'list-right'); + removeClass(this.element, 'docs-below'); + + if (spaceOntheRight > this.maxWidgetWidth) { + // There is enough space on the right, so nothing to do here. + return; + } + + + if (spaceOntheRight > this.minWidgetWidth) { + // There is enough space on the right for list and resized docs + this.adjustDocs(false, spaceOntheRight - this.listWidth, this.editor.getPosition().column); + return; + } + + if (spaceOntheRight > this.listWidth && spaceOntheLeft > this.maxDocsWidth) { + // Docs on the left and list on the right of the cursor + let columnsOccupiedByDocs = Math.floor(this.maxDocsWidth / perColumnWidth); + this.adjustDocs(true, null, this.editor.getPosition().column - columnsOccupiedByDocs); + return; + } + + if (spaceOntheRight > this.listWidth && spaceOntheLeft > this.minDocsWidth) { + // Resized docs on the left and list on the right of the cursor + let columnsOccupiedByDocs = Math.floor(spaceOntheLeft / perColumnWidth); + this.adjustDocs(true, spaceOntheLeft, this.editor.getPosition().column - columnsOccupiedByDocs); + return; + } + + if (this.editor.getLayoutInfo().contentWidth > this.maxWidgetWidth) { + // Use as much space on the right, and for the rest go left + let columnsOccupiedByWidget = Math.floor(this.maxWidgetWidth / perColumnWidth); + let preferredColumn = this.editor.getLayoutInfo().viewportColumn - columnsOccupiedByWidget + scrolledColumns; + this.adjustDocs(true, null, preferredColumn); + return; + } + + if (this.editor.getLayoutInfo().contentWidth > this.minWidgetWidth) { + // Resize docs. Swap only of there is enough space on the right for the list + let newDocsWidth = this.editor.getLayoutInfo().contentWidth - this.listWidth; + this.adjustDocs(spaceOntheRight < this.listWidth, newDocsWidth, scrolledColumns); + return; + } + + // Not enough space to show side by side + // So show docs below the list + addClass(this.element, 'docs-below'); + this.listElement.style.width = `${this.widgetWidthInSmallestEditor}px`; + this.element.style.width = `${this.widgetWidthInSmallestEditor}px`; + this.details.element.style.width = `${this.widgetWidthInSmallestEditor}px`; + } + + /** + * Adjust the width of the docs widget, swaps docs/list and moves suggest widget if needed + * + * @swap boolean If true, then the docs and list are swapped + * @resizedDocWidth number If not null, this number will be used to set the width of the docs + * @preferredColumn Preferred column in the current line for the suggest widget + */ + private adjustDocs(swap: boolean, resizedDocWidth: number, preferredColumn: number) { + + if (swap) { + addClass(this.element, 'list-right'); + } + + if (resizedDocWidth !== null) { + this.details.element.style.width = `${resizedDocWidth}px`; + this.element.style.width = `${resizedDocWidth + this.listWidth}px`; + } + + this.preferredPosition = new Position(this.editor.getPosition().lineNumber, preferredColumn); + } + private renderDetails(): void { - if (this.state !== State.Details) { - this.details.render(null); - } else { + if (this.state === State.Details || this.state === State.Open) { this.details.render(this.list.getFocusedElements()[0]); + } else { + this.details.render(null); } } @@ -894,10 +924,6 @@ export class SuggestWidget implements IContentWidget, IDelegate // IDelegate getHeight(element: ICompletionItem): number { - if (canExpandCompletionItem(element) && element === this.focusedItem) { - return this.focusHeight; - } - return this.unfocusedHeight; }