Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Commit

Permalink
Add an API for Markdown-based datatips
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hansonw authored and facebook-github-bot committed Apr 5, 2017
1 parent f45f3d4 commit 2014073
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 89 deletions.
9 changes: 7 additions & 2 deletions pkg/nuclide-datatip/lib/DatatipComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -63,8 +64,12 @@ export class DatatipComponent extends React.Component {
/>
);
}
const ProvidedComponent = datatip.component;
const content = <ProvidedComponent />;
let content;
if (datatip.component != null) {
content = <datatip.component />;
} else if (datatip.markedStrings != null) {
content = <MarkedStringDatatip markedStrings={datatip.markedStrings} />;
}
return (
<div
className={`${maybeToString(className)} nuclide-datatip-container`}
Expand Down
48 changes: 48 additions & 0 deletions pkg/nuclide-datatip/lib/MarkedStringDatatip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*
* @flow
*/

import type {MarkedString} from './types';

import marked from 'marked';
import React from 'react';

import MarkedStringSnippet from './MarkedStringSnippet';

type Props = {
markedStrings: Array<MarkedString>,
};

export default class MarkedStringDatatip extends React.PureComponent {
props: Props;

render(): React.Element<any> {
const elements = this.props.markedStrings.map((chunk, i) => {
if (chunk.type === 'markdown') {
return (
<div
className="nuclide-datatip-marked-container"
dangerouslySetInnerHTML={{
__html: marked(chunk.value, {sanitize: true}),
}}
key={i}
/>
);
} else {
return <MarkedStringSnippet key={i} {...chunk} />;
}
});

return (
<div className="nuclide-datatip-marked">
{elements}
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
return () => <TypeHintComponent content={content} grammar={grammar} />;
}

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<any> {
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 (
<div
className="nuclide-type-hint-text-editor-container"
className="nuclide-datatip-marked-text-editor-container"
onClick={(e: SyntheticEvent) => {
this.setState({isExpanded: !this.state.isExpanded});
e.stopPropagation();
}}>
<AtomTextEditor
className="nuclide-type-hint-text-editor"
className="nuclide-datatip-marked-text-editor"
gutterHidden={true}
readOnly={true}
syncTextContents={false}
Expand Down
28 changes: 23 additions & 5 deletions pkg/nuclide-datatip/lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,29 @@
* @flow
*/

export type Datatip = {
component: ReactClass<any>,
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<any>,
range: atom$Range,
pinnable?: boolean,
|}
| {|
markedStrings: Array<MarkedString>,
range: atom$Range,
pinnable?: boolean,
|};

export type PinnedDatatip = {
dispose(): void,
Expand Down
44 changes: 44 additions & 0 deletions pkg/nuclide-datatip/styles/nuclide-datatip-marked.less
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 1 addition & 2 deletions pkg/nuclide-type-hint/lib/TypeHintManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -59,7 +58,7 @@ export default class TypeHintManager {
message: hint,
});
return {
component: makeTypeHintComponent(hint, grammar),
markedStrings: [{type: 'snippet', value: hint, grammar}],
range,
};
}
Expand Down
49 changes: 0 additions & 49 deletions pkg/nuclide-type-hint/styles/nuclide-type-hint.less

This file was deleted.

38 changes: 37 additions & 1 deletion pkg/sample-datatip/lib/SampleDatatip.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {makeSampleDatatipComponent} from './SampleDatatipComponent';

const WORD_REGEX = /\w+/gi;

export async function datatip(editor: TextEditor, position: atom$Point): Promise<?Datatip> {
export async function datatip(
editor: TextEditor,
position: atom$Point,
): Promise<?Datatip> {
const extractedWord = wordAtPosition(editor, position, WORD_REGEX);
if (extractedWord == null) {
return null;
Expand All @@ -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,
};
Expand Down

0 comments on commit 2014073

Please sign in to comment.