diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 5385d73aca..8d3bef967f 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -49,6 +49,7 @@ - #3404 - UX - Explorer: Fix text overflow in explorer (fixes #3362) - #3414 - Code Actions: Fix regression in control-p binding - #3416 - Editor: Fix word-wrap calculation with tab characters (fixes #3372) +- #2329 - UX - Completion: Fix overflowing detail text (fixes #2264) ### Performance diff --git a/src/Feature/Editor/OverlaysView.re b/src/Feature/Editor/OverlaysView.re index 289e996b44..b43a21cc39 100644 --- a/src/Feature/Editor/OverlaysView.re +++ b/src/Feature/Editor/OverlaysView.re @@ -27,6 +27,7 @@ let completionsView = ~buffer, ~cursor, ~languageSupport, + ~lineHeight, ~cursorPixelX, ~cursorPixelY, ~theme, @@ -40,7 +41,7 @@ let completionsView = cursor x=cursorPixelX y=cursorPixelY - lineHeight={editorFont.measuredHeight} + lineHeight theme tokenTheme editorFont @@ -101,12 +102,14 @@ let make = let cursorPixelY = pixelY |> int_of_float; let cursorPixelX = pixelX +. gutterWidth |> int_of_float; + let lineHeight = Editor.lineHeightInPixels(editor); isActiveSplit ? { @@ -484,6 +488,10 @@ let configurationChanged = (~config, model) => { ...model, acceptOnEnter: CompletionConfig.acceptSuggestionOnEnter.get(config), snippetSortOrder: CompletionConfig.snippetSuggestions.get(config), + isAnimationEnabled: + Feature_Configuration.GlobalConfiguration.animation.get(config), + isShadowEnabled: + Feature_Configuration.GlobalConfiguration.shadows.get(config), }; }; @@ -1076,8 +1084,6 @@ module View = { module Constants = { let maxCompletionWidth = 225; let maxDetailWidth = 225; - let itemHeight = 22; - let maxHeight = itemHeight * 5; let opacity = 1.0; let padding = 8; }; @@ -1172,32 +1178,46 @@ module View = { top(int_of_float(lineHeight +. 0.5)), left(0), Style.width(width), - Style.height(height), + Style.height(height + 2), // Add 2 to account for border! border(~color=colors.suggestWidgetBorder, ~width=1), backgroundColor(colors.suggestWidgetBackground), ]; - let item = (~isFocused, ~colors: Colors.t) => [ + let item = (~rowHeight, ~isFocused, ~colors: Colors.t) => [ isFocused ? backgroundColor(colors.suggestWidgetSelectedBackground) : backgroundColor(colors.suggestWidgetBackground), flexDirection(`Row), + height(rowHeight), + justifyContent(`Center), ]; - let icon = (~color) => [ + let icon = (~size, ~color) => [ flexDirection(`Row), justifyContent(`Center), alignItems(`Center), flexGrow(0), + flexShrink(0), backgroundColor(color), - width(25), - padding(4), + width(size), + height(size), ]; - let label = [flexGrow(1), margin(4)]; + let label = [ + flexGrow(1), + flexShrink(1), + marginTop(2), + marginHorizontal(4), + ]; + + let detail = [ + flexGrow(2), + flexShrink(2), + marginTop(2), + marginHorizontal(4), + ]; let text = (~highlighted=false, ~colors: Colors.t, ()) => [ - textOverflow(`Ellipsis), textWrap(Revery.TextWrapping.NoWrap), color( highlighted ? colors.normalModeBackground : colors.editorForeground, @@ -1206,22 +1226,19 @@ module View = { let highlightedText = (~colors) => text(~highlighted=true, ~colors, ()); - let detail = (~width, ~lineHeight, ~colors: Colors.t) => [ + let documentation = (~width, ~lineHeight, ~colors: Colors.t) => [ position(`Absolute), left(width), top(int_of_float(lineHeight +. 0.5)), Style.width(Constants.maxDetailWidth), - flexDirection(`Column), - alignItems(`FlexStart), - justifyContent(`Center), border(~color=colors.suggestWidgetBorder, ~width=1), backgroundColor(colors.suggestWidgetBackground), ]; let detailText = (~tokenTheme: TokenTheme.t) => [ textOverflow(`Ellipsis), + textWrap(Revery.TextWrapping.NoWrap), color(tokenTheme.commentColor), - margin(3), ]; }; @@ -1231,6 +1248,10 @@ module View = { ~text, ~kind, ~highlight, + ~detail: option(string), + ~tokenTheme, + ~rowHeight, + ~width, ~theme: Oni_Core.ColorTheme.Colors.t, ~colors: Colors.t, ~editorFont: Service_Font.font, @@ -1240,8 +1261,57 @@ module View = { let iconColor = kind |> kindToColor(theme); - - + let textWidth = Service_Font.measure(~text, editorFont); + let labelWidth = int_of_float(ceil(textWidth +. 0.5)); + + let padding = rowHeight; + let availableWidth = width - rowHeight - padding - labelWidth; + let remainingWidth = availableWidth > 50 ? availableWidth : 0; + + let textMarginTop = 2; + + let maybeDetail = + switch (detail) { + | Some(detail) when isFocused && remainingWidth > 0 => + + + + + + + | _ => React.empty + }; + + + - + + maybeDetail ; }; let detailView = ( - ~text, + ~documentation, ~width, ~lineHeight, + ~uiFont: Oni_Core.UiFont.t, ~editorFont: Service_Font.font, ~colors, + ~colorTheme, ~tokenTheme, (), - ) => - - + ) => { + let documentationElement = + Markdown.make( + ~markdown=Exthost.MarkdownString.toString(documentation), + ~fontFamily=uiFont.family, + ~colorTheme, + ~tokenTheme, + ~languageInfo=Exthost.LanguageInfo.initial, + ~defaultLanguage="reason", + ~codeFontFamily=editorFont.fontFamily, + ~grammars=Oni_Syntax.GrammarRepository.empty, + ~baseFontSize=10., + ~codeBlockFontSize=editorFont.fontSize, + (), + ); + + + + documentationElement + ; + }; let make = ( @@ -1317,41 +1411,61 @@ module View = { let focused = completions.selection; - let maxWidth = - items - |> Array.fold_left( - (maxWidth, this: CompletionItem.t) => { - let textWidth = - Service_Font.measure(~text=this.label, editorFont); - let thisWidth = - int_of_float(textWidth +. 0.5) + Constants.padding; - max(maxWidth, thisWidth); - }, - Constants.maxCompletionWidth, - ); - - let width = maxWidth + Constants.padding * 2; - let height = - min(Constants.maxHeight, Array.length(items) * Constants.itemHeight); - - let detail = - switch (focused) { - | Some(index) => - let focused: CompletionItem.t = items[index]; - switch (focused.detail) { - | Some(text) => - - | None => React.empty - }; - | None => React.empty + let width = 500; + let itemHeight = int_of_float(ceil(lineHeight)); + let maxHeight = itemHeight * 5; + let height = min(maxHeight, Array.length(items) * itemHeight); + + // TODO: Bring back detail view: + // 1) Align underneath completion + // 2) Test with providers that have large details (like Python) + let detail = React.empty; + // let detail = + // switch (focused) { + // | Some(index) => + // let focused: CompletionItem.t = items[index]; + // switch (focused.documentation) { + // | Some(documentation) => + // + // | None => React.empty + // }; + // | None => React.empty + // }; + + let innerStyle = + Styles.innerPosition(~height, ~width, ~lineHeight, ~colors); + + let innerStyleWithShadow = + if (completions.isShadowEnabled) { + let color = Feature_Theme.Colors.shadow.from(theme); + [ + Style.boxShadow( + ~xOffset=4., + ~yOffset=4., + ~blurRadius=12., + ~spreadRadius=0., + ~color, + ), + ...innerStyle, + ]; + } else { + innerStyle; }; - + ; diff --git a/src/Feature/LanguageSupport/CompletionItem.re b/src/Feature/LanguageSupport/CompletionItem.re index 6c81e3d4a9..8dc0532d8f 100644 --- a/src/Feature/LanguageSupport/CompletionItem.re +++ b/src/Feature/LanguageSupport/CompletionItem.re @@ -83,8 +83,8 @@ let snippet = (~meet, ~base, ~prefix: string, snippet: string) => { handle: None, label: prefix, kind: Exthost.CompletionKind.Snippet, - detail: Some(snippet), - documentation: None, + detail: Some("Snippet"), + documentation: Some(Exthost.MarkdownString.fromString(snippet)), insertText: snippet, insertTextRules: Exthost.SuggestItem.InsertTextRules.insertAsSnippet, filterText: prefix,