From 20140733bc0608a6fee292b7a15c9cdf1776adfe Mon Sep 17 00:00:00 2001 From: Hanson Wang Date: Tue, 4 Apr 2017 18:14:22 -0700 Subject: [PATCH] Add an API for Markdown-based datatips Summary: The LSP's `hover` API (equivalent of datatip) works with Markdown strings and code snippets. https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_hover This is actually quite convenient for external providers as this alleviates the React requirement. Adopt this API (`MarkedString`) and use it for typehints. I actually just moved the type hint UI into datatips in order to easily implement the code snippet part. Here's a screenshot of the sample: https://pxl.cl/73Wm Reviewed By: jgebhardt Differential Revision: D4812360 fbshipit-source-id: d191aeac5f0c92e5c056a3e191c73a4cd2d6f199 --- pkg/nuclide-datatip/lib/DatatipComponent.js | 9 +++- .../lib/MarkedStringDatatip.js | 48 ++++++++++++++++++ .../lib/MarkedStringSnippet.js} | 41 +++++----------- pkg/nuclide-datatip/lib/types.js | 28 +++++++++-- .../styles/nuclide-datatip-marked.less | 44 +++++++++++++++++ pkg/nuclide-type-hint/lib/TypeHintManager.js | 3 +- .../styles/nuclide-type-hint.less | 49 ------------------- pkg/sample-datatip/lib/SampleDatatip.js | 38 +++++++++++++- 8 files changed, 171 insertions(+), 89 deletions(-) create mode 100644 pkg/nuclide-datatip/lib/MarkedStringDatatip.js rename pkg/{nuclide-type-hint/lib/TypeHintComponent.js => nuclide-datatip/lib/MarkedStringSnippet.js} (58%) create mode 100644 pkg/nuclide-datatip/styles/nuclide-datatip-marked.less delete mode 100644 pkg/nuclide-type-hint/styles/nuclide-type-hint.less diff --git a/pkg/nuclide-datatip/lib/DatatipComponent.js b/pkg/nuclide-datatip/lib/DatatipComponent.js index dcca69f063..7b128b8aa1 100644 --- a/pkg/nuclide-datatip/lib/DatatipComponent.js +++ b/pkg/nuclide-datatip/lib/DatatipComponent.js @@ -12,6 +12,7 @@ import type {Datatip} from './types'; import React from 'react'; import {maybeToString} from '../../commons-node/string'; +import MarkedStringDatatip from './MarkedStringDatatip'; export const DATATIP_ACTIONS = Object.freeze({ PIN: 'PIN', @@ -63,8 +64,12 @@ export class DatatipComponent extends React.Component { /> ); } - const ProvidedComponent = datatip.component; - const content = ; + let content; + if (datatip.component != null) { + content = ; + } else if (datatip.markedStrings != null) { + content = ; + } return (
, +}; + +export default class MarkedStringDatatip extends React.PureComponent { + props: Props; + + render(): React.Element { + const elements = this.props.markedStrings.map((chunk, i) => { + if (chunk.type === 'markdown') { + return ( +
+ ); + } else { + return ; + } + }); + + return ( +
+ {elements} +
+ ); + } +} diff --git a/pkg/nuclide-type-hint/lib/TypeHintComponent.js b/pkg/nuclide-datatip/lib/MarkedStringSnippet.js similarity index 58% rename from pkg/nuclide-type-hint/lib/TypeHintComponent.js rename to pkg/nuclide-datatip/lib/MarkedStringSnippet.js index 32b1e1a2a4..b0bc59e277 100644 --- a/pkg/nuclide-type-hint/lib/TypeHintComponent.js +++ b/pkg/nuclide-datatip/lib/MarkedStringSnippet.js @@ -15,49 +15,30 @@ import {AtomTextEditor} from '../../nuclide-ui/AtomTextEditor'; // Complex types can end up being super long. Truncate them. const MAX_LENGTH = 100; -type TypeHintComponentProps = { - content: string, - grammar: atom$Grammar, -}; - -type TypeHintComponentState = { - isExpanded: boolean, -}; - -export function makeTypeHintComponent( - content: string, - grammar: atom$Grammar, -): ReactClass { - return () => ; -} - -class TypeHintComponent extends React.Component { - props: TypeHintComponentProps; - state: TypeHintComponentState; - - constructor(props: TypeHintComponentProps) { - super(props); - this.state = { - isExpanded: false, - }; - } +export default class MarkedStringSnippet extends React.Component { + props: { + value: string, + grammar: atom$Grammar, + }; + state = { + isExpanded: false, + }; render(): React.Element { - const value = this.props.content; + const {value, grammar} = this.props; const shouldTruncate = value.length > MAX_LENGTH && !this.state.isExpanded; const buffer = new TextBuffer( shouldTruncate ? value.substr(0, MAX_LENGTH) + '...' : value, ); - const {grammar} = this.props; return (
{ this.setState({isExpanded: !this.state.isExpanded}); e.stopPropagation(); }}> , - range: atom$Range, - pinnable?: boolean, -}; +// Borrowed from the LSP API. +export type MarkedString = + | { + type: 'markdown', + value: string, + } + | { + type: 'snippet', + grammar: atom$Grammar, + value: string, + }; + +export type Datatip = + | {| + component: ReactClass, + range: atom$Range, + pinnable?: boolean, + |} + | {| + markedStrings: Array, + range: atom$Range, + pinnable?: boolean, + |}; export type PinnedDatatip = { dispose(): void, diff --git a/pkg/nuclide-datatip/styles/nuclide-datatip-marked.less b/pkg/nuclide-datatip/styles/nuclide-datatip-marked.less new file mode 100644 index 0000000000..e0ee99d746 --- /dev/null +++ b/pkg/nuclide-datatip/styles/nuclide-datatip-marked.less @@ -0,0 +1,44 @@ +@import "ui-variables"; +@import "syntax-variables"; + +.nuclide-datatip-marked > div:not(:last-child) { + border-bottom: 1px solid fade(@text-color-highlight, 10%); +} + +.nuclide-datatip-marked-container { + color: @text-color; + font-family: @font-family; + padding: 8px; + + // Avoid excess internal padding from markdown blocks. + :first-child { + margin-top: 0; + } + + :last-child { + margin-bottom: 0; + } +} + +.nuclide-datatip-marked-text-editor { + max-height: 300px; + overflow-y: auto; +} + +.nuclide-datatip-marked-text-editor atom-text-editor { + background-color: transparent; + min-width: 1em; // Hack to force the AtomTextEditor to properly size itself + padding: 2px 0 2px 4px; + + .editor-contents--private { + cursor: inherit!important; // Let the enclosing datatip override the cursor. + } + + // Prevent forced scroll bar in Atom 1.9.x + .scroll-view .horizontal-scrollbar { + display: none; + } + .scrollbar-corner { + display: none; + } +} diff --git a/pkg/nuclide-type-hint/lib/TypeHintManager.js b/pkg/nuclide-type-hint/lib/TypeHintManager.js index a973e03e74..e4fd8ce776 100644 --- a/pkg/nuclide-type-hint/lib/TypeHintManager.js +++ b/pkg/nuclide-type-hint/lib/TypeHintManager.js @@ -13,7 +13,6 @@ import type {Datatip} from '../../nuclide-datatip/lib/types'; import {arrayRemove} from '../../commons-node/collection'; import {track, trackTiming} from '../../nuclide-analytics'; -import {makeTypeHintComponent} from './TypeHintComponent'; import {getLogger} from '../../nuclide-logging'; const logger = getLogger(); @@ -59,7 +58,7 @@ export default class TypeHintManager { message: hint, }); return { - component: makeTypeHintComponent(hint, grammar), + markedStrings: [{type: 'snippet', value: hint, grammar}], range, }; } diff --git a/pkg/nuclide-type-hint/styles/nuclide-type-hint.less b/pkg/nuclide-type-hint/styles/nuclide-type-hint.less deleted file mode 100644 index 866ac2a98e..0000000000 --- a/pkg/nuclide-type-hint/styles/nuclide-type-hint.less +++ /dev/null @@ -1,49 +0,0 @@ -@import "ui-variables"; - -.nuclide-type-hint-overlay { - background: @app-background-color; - box-shadow: 0px 1px 4px 0px rgba(0,0,0,1); - color: rgb(103, 103, 103); - font-family: Menlo, Monaco, Consolas, monospace; - font-size: @font-size; - padding: 4px 10px; - position: relative; - white-space: nowrap; -} - -.nuclide-type-hint-expandable-chevron { - cursor: pointer; -} - -.nuclide-type-hint-text-editor-container { - display: flex; - flex-grow: 1; - max-width: 60em; -} - -.nuclide-type-hint-text-editor { - max-height: 300px; - overflow-y: auto; -} - -.nuclide-type-hint-text-editor atom-text-editor { - background-color: transparent; - min-width: 1em; // Hack to force the AtomTextEditor to properly size itself - padding: 2px 0 2px 4px; - - .editor-contents--private { - cursor: inherit!important; // Let the enclosing datatip override the cursor. - } - - // Prevent forced scroll bar in Atom 1.9.x - .scroll-view .horizontal-scrollbar { - display: none; - } - .scrollbar-corner { - display: none; - } -} - -atom-text-editor .nuclide-type-hint-highlight-region > .region { - background: @background-color-highlight; -} diff --git a/pkg/sample-datatip/lib/SampleDatatip.js b/pkg/sample-datatip/lib/SampleDatatip.js index cc6badee18..76e2e12234 100644 --- a/pkg/sample-datatip/lib/SampleDatatip.js +++ b/pkg/sample-datatip/lib/SampleDatatip.js @@ -15,7 +15,10 @@ import {makeSampleDatatipComponent} from './SampleDatatipComponent'; const WORD_REGEX = /\w+/gi; -export async function datatip(editor: TextEditor, position: atom$Point): Promise { +export async function datatip( + editor: TextEditor, + position: atom$Point, +): Promise { const extractedWord = wordAtPosition(editor, position, WORD_REGEX); if (extractedWord == null) { return null; @@ -25,7 +28,40 @@ export async function datatip(editor: TextEditor, position: atom$Point): Promise range, } = extractedWord; const word = wordMatch[0] == null ? 'N/A' : wordMatch[0]; + if (editor.getGrammar().scopeName === 'source.gfm') { + // Demo of the Markdown string-based API. + return { + markedStrings: [ + { + type: 'markdown', + value: `An h1 header +============ + +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic*, **bold**, and \`monospace\`. Itemized lists +look like: + + * this one + * that one + * the other one`, + }, + { + type: 'snippet', + grammar: atom.grammars.selectGrammar('js', ''), + value: 'function f(x: number): boolean {', + }, + { + type: 'snippet', + grammar: atom.grammars.selectGrammar('js', ''), + value: 'function f: (number) => boolean', + }, + ], + range, + }; + } return { + // For more complex use cases, provide a custom React component. component: makeSampleDatatipComponent(word), range, };