From e0969a51364576eab0677c067cb0bff320d72f3e Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Wed, 17 May 2017 11:13:09 -0700 Subject: [PATCH] Toggle expanded docs, add stickyness to expansion (#26783) * Toggle expanded docs, add stickyness to expansion * New command for moving focus to/from details * Store user choice in local storage * Refactoring * Show prev width for suggest list unless docs expand * Add keybinding to Read Less button --- .../contrib/suggest/browser/media/suggest.css | 82 +++++++++++-- .../suggest/browser/suggestController.ts | 18 +++ .../contrib/suggest/browser/suggestWidget.ts | 108 +++++++++++++++--- 3 files changed, 184 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/contrib/suggest/browser/media/suggest.css b/src/vs/editor/contrib/suggest/browser/media/suggest.css index 2329d006eab..ad198d8aa3f 100644 --- a/src/vs/editor/contrib/suggest/browser/media/suggest.css +++ b/src/vs/editor/contrib/suggest/browser/media/suggest.css @@ -9,15 +9,16 @@ width: 660px; } -.monaco-editor .suggest-widget > .tree, +.monaco-editor .suggest-widget.docs-expanded > .tree, .monaco-editor .suggest-widget > .details { width: 330px; } .monaco-editor .suggest-widget.small, -.monaco-editor .suggest-widget.small > .tree, +.monaco-editor .suggest-widget > .tree, +.monaco-editor .suggest-widget.small.docs-expanded > .tree, .monaco-editor .suggest-widget.small > .details { - width: 400px; + width: 430px; } .monaco-editor .suggest-widget.visible { @@ -72,6 +73,55 @@ font-weight: bold; } +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .go-back, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .docs-details { + opacity: 0.6; + background-position: center center; + background-repeat: no-repeat; + background-size: 70%; + color: #0035DD; + cursor: pointer; +} + +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .go-back { + background-image: url('./back.svg'); +} + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .docs-details { + background-image: url('./info.svg'); +} + +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .go-back:hover, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .docs-details:hover { + opacity: 1; +} + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .type-label { + 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 > .contents > .main > .docs-details, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .type-label, +.monaco-editor .suggest-widget.docs-expanded .monaco-list .monaco-list-row.focused > .contents > .main > .docs-details, +.monaco-editor .suggest-widget.docs-expanded .monaco-list .monaco-list-row.focused > .contents > .main > .type-label { + display: none; +} + +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused > .contents > .main > .docs-details, +.monaco-editor .suggest-widget .monaco-list .monaco-list-row.focused > .contents > .main > .type-label, +.monaco-editor .suggest-widget.docs-expanded.small .monaco-list .monaco-list-row.focused > .contents > .main > .type-label { + display: inline; +} + .monaco-editor .suggest-widget .monaco-list .monaco-list-row .icon { display: block; height: 16px; @@ -141,9 +191,20 @@ white-space: pre-wrap; } -.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .type { +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header { + padding: 4px 5px; + display: flex; + box-sizing: border-box; + border-bottom: 1px solid rgba(204, 204, 204, 0.5); +} + +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .type { + flex: 2; + overflow: hidden; + text-overflow: ellipsis; opacity: 0.7; word-break: break-all; + margin: 0; } .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > p { @@ -155,12 +216,19 @@ display: none; } -/* High Contrast Theming */ +/* Dark theme */ -.monaco-editor.hc-black .suggest-widget .details > .monaco-scrollable-element > .body > .docs { - color: #C07A7A; +.monaco-editor.vs-dark .suggest-widget .details > .monaco-scrollable-element > .body > .header { + border-color: rgba(85,85,85,0.5); } +.monaco-editor.vs-dark .suggest-widget .details > .monaco-scrollable-element > .body > .header > .go-back, +.monaco-editor.vs-dark .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .docs-details { + color: #4E94CE; +} + +/* High Contrast Theming */ + .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 04252c5d78c..0468b72476d 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -262,6 +262,12 @@ export class SuggestController implements IEditorContribution { } toggleSuggestionDetails(): void { + if (this._widget) { + this._widget.toggleDetails(); + } + } + + toggleSuggestionFocus(): void { if (this._widget) { this._widget.toggleDetailsFocus(); } @@ -408,3 +414,15 @@ CommonEditorRegistry.registerEditorCommand(new SuggestCommand({ mac: { primary: KeyMod.WinCtrl | KeyCode.Space } } })); + +CommonEditorRegistry.registerEditorCommand(new SuggestCommand({ + id: 'toggleSuggestionFocus', + precondition: SuggestContext.Visible, + handler: x => x.toggleSuggestionFocus(), + kbOpts: { + weight: weight, + kbExpr: EditorContextKeys.textFocus, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space } + } +})); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 2a169a02ec7..6b52946e154 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -13,7 +13,7 @@ import Event, { Emitter, chain } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition } from 'vs/base/browser/dom'; +import { addClass, append, $, hide, removeClass, show, toggleClass, getDomNodePagePosition, hasClass } from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IDelegate, IListEvent, IRenderer } from 'vs/base/browser/ui/list/list'; import { List } from 'vs/base/browser/ui/list/listWidget'; @@ -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, listFocusBackground, activeContrastBorder, listHighlightForeground, editorForeground, editorWidgetBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; const sticky = false; // for development purposes @@ -38,6 +39,8 @@ interface ISuggestionTemplateData { icon: HTMLElement; colorspan: HTMLElement; highlightedLabel: HighlightedLabel; + typeLabel: HTMLElement; + documentationDetails: HTMLElement; disposables: IDisposable[]; } @@ -66,15 +69,12 @@ function canExpandCompletionItem(item: ICompletionItem) { class Renderer implements IRenderer { - private triggerKeybindingLabel: string; - constructor( private widget: SuggestWidget, private editor: ICodeEditor, - @IKeybindingService keybindingService: IKeybindingService + private triggerKeybindingLabel: string ) { - const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest'); - this.triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`; + } get templateId(): string { @@ -93,6 +93,11 @@ 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')); + + data.documentationDetails = append(main, $('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; @@ -106,6 +111,8 @@ 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(); @@ -139,7 +146,24 @@ class Renderer implements IRenderer { } data.highlightedLabel.set(suggestion.label, createMatches(element.matches)); + data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, ''); + 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; + } } @@ -161,6 +185,8 @@ const enum State { class SuggestionDetails { private el: HTMLElement; + private back: HTMLElement; + private header: HTMLElement; private scrollbar: DomScrollableElement; private body: HTMLElement; private type: HTMLElement; @@ -171,7 +197,8 @@ class SuggestionDetails { constructor( container: HTMLElement, private widget: SuggestWidget, - private editor: ICodeEditor + private editor: ICodeEditor, + private triggerKeybindingLabel: string ) { this.disposables = []; @@ -184,7 +211,12 @@ class SuggestionDetails { append(this.el, this.scrollbar.getDomNode()); this.disposables.push(this.scrollbar); - this.type = append(this.body, $('p.type')); + this.header = append(this.body, $('.header')); + this.type = append(this.header, $('p.type')); + + this.back = append(this.header, $('span.go-back')); + this.back.title = nls.localize('goback', "Read less...{0}", triggerKeybindingLabel); + this.docs = append(this.body, $('p.docs')); this.ariaLabel = null; @@ -211,7 +243,17 @@ class SuggestionDetails { this.type.innerText = item.suggestion.detail || ''; this.docs.textContent = item.suggestion.documentation; - this.el.style.height = this.type.clientHeight + this.docs.clientHeight + 'px'; + this.el.style.height = this.type.clientHeight + this.docs.clientHeight + 8 + 'px'; + + this.back.onmousedown = e => { + e.preventDefault(); + e.stopPropagation(); + }; + this.back.onclick = e => { + e.preventDefault(); + e.stopPropagation(); + this.widget.toggleDetails(); + }; this.body.scrollTop = 0; this.scrollbar.scanDomNode(); @@ -251,10 +293,14 @@ 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.type.style.fontFamily = fontFamily; + this.back.style.height = lineHeightPx; + this.back.style.width = lineHeightPx; } dispose(): void { @@ -308,18 +354,24 @@ export class SuggestWidget implements IContentWidget, IDelegate private readonly maxWidgetWidth = 660; private readonly listWidth = 330; - private readonly minWidgetWidth = 400; + private readonly minWidgetWidth = 430; + private storageService: IStorageService; constructor( private editor: ICodeEditor, @ITelemetryService private telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IKeybindingService keybindingService: IKeybindingService ) { + const kb = keybindingService.lookupKeybinding('editor.action.triggerSuggest'); + const triggerKeybindingLabel = !kb ? '' : ` (${kb.getLabel()})`; + this.isAuto = false; this.focusedItem = null; - + this.storageService = storageService; this.element = $('.editor-widget.suggest-widget'); if (!this.editor.getConfiguration().contribInfo.iconsInSuggestions) { @@ -328,9 +380,9 @@ export class SuggestWidget implements IContentWidget, IDelegate this.messageElement = append(this.element, $('.message')); this.listElement = append(this.element, $('.tree')); - this.details = new SuggestionDetails(this.element, this, this.editor); + this.details = new SuggestionDetails(this.element, this, this.editor, triggerKeybindingLabel); - let renderer: IRenderer = instantiationService.createInstance(Renderer, this, this.editor); + let renderer: IRenderer = instantiationService.createInstance(Renderer, this, this.editor, triggerKeybindingLabel); this.list = new List(this.listElement, this, [renderer], { useShadows: false, @@ -547,8 +599,11 @@ export class SuggestWidget implements IContentWidget, IDelegate this.show(); break; case State.Open: - hide(this.messageElement); - show(this.listElement, this.details.element); + hide(this.messageElement, this.details.element); + show(this.listElement); + if (this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, false)) { + show(this.details.element); + } this.show(); break; case State.Frozen: @@ -740,10 +795,26 @@ export class SuggestWidget implements IContentWidget, IDelegate } } + toggleDetails(): void { + let expandDocs = this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, false); + if (expandDocs) { + hide(this.details.element); + removeClass(this.element, 'docs-expanded'); + } else { + show(this.details.element); + addClass(this.element, 'docs-expanded'); + this.show(); + } + this.storageService.store('expandSuggestionDocs', !expandDocs, StorageScope.GLOBAL); + } + showDetails(): void { if (this.state !== State.Open && this.state !== State.Details) { return; } + if (this.storageService.getBoolean('expandSuggestionDocs', StorageScope.GLOBAL, false)) { + addClass(this.element, 'docs-expanded'); + } this.show(); this.editor.focus(); @@ -792,7 +863,10 @@ export class SuggestWidget implements IContentWidget, IDelegate private updateWidgetHeight(): number { let height = 0; - let maxSuggestionsToShow = this.editor.getLayoutInfo().contentWidth > this.minWidgetWidth ? 11 : 5; + let maxSuggestionsToShow = 11; + if (hasClass(this.element, 'small')) { + maxSuggestionsToShow = 5; + } if (this.state === State.Empty || this.state === State.Loading) { height = this.unfocusedHeight; -- GitLab