import React, {Component, PropTypes} from 'react';
import {getDOMNode} from 'react-dom';
import CodeMirror from 'codemirror';
import classnames from 'classnames';
import JSONPath from 'jsonpath-plus';
import vkBeautify from 'vkbeautify';
import {DOMParser} from 'xmldom';
import xpath from 'xpath';
import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/go/go';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/mllike/mllike';
import 'codemirror/mode/php/php';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/python/python';
import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/swift/swift';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/fold/xml-fold';
import 'codemirror/addon/display/autorefresh';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/search/matchesonscrollbar';
import 'codemirror/addon/search/matchesonscrollbar.css';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
import 'codemirror/addon/lint/lint.css';
import '../../css/components/editor.less';
import {showModal} from '../modals/index';
import AlertModal from '../modals/AlertModal';
import Link from '../base/Link';
import * as misc from '../../../common/misc';
import {trackEvent} from '../../../analytics/index';
const BASE_CODEMIRROR_OPTIONS = {
lineNumbers: true,
placeholder: 'Start Typing...',
foldGutter: true,
height: 'auto',
autoRefresh: {delay: 250}, // Necessary to show up in the env modal first launch
lineWrapping: true,
lint: true,
tabSize: 4,
matchBrackets: true,
autoCloseBrackets: true,
indentUnit: 4,
indentWithTabs: true,
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
'CodeMirror-lint-markers'
],
cursorScrollMargin: 12, // NOTE: This is px
extraKeys: {
'Ctrl-Q': function (cm) {
cm.foldCode(cm.getCursor());
}
}
};
class Editor extends Component {
constructor (props) {
super(props);
this.state = {
filter: props.filter || ''
};
this._originalCode = '';
}
componentWillUnmount () {
// todo: is there a lighter-weight way to remove the cm instance?
if (this.codeMirror) {
this.codeMirror.toTextArea();
}
}
/**
* Focus the cursor to the editor
*/
focus () {
if (this.codeMirror) {
this.codeMirror.focus();
}
}
selectAll () {
if (this.codeMirror) {
this.codeMirror.setSelection(
{line: 0, ch: 0},
{line: this.codeMirror.lineCount(), ch: 0}
);
}
}
getValue () {
return this.codeMirror.getValue();
}
_handleInitTextarea = textarea => {
if (!textarea) {
// Not mounted
return;
}
if (this.codeMirror) {
// Already initialized
return;
}
const {value} = this.props;
this.codeMirror = CodeMirror.fromTextArea(textarea, BASE_CODEMIRROR_OPTIONS);
this.codeMirror.on('change', misc.debounce(this._codemirrorValueChanged.bind(this)));
this.codeMirror.on('paste', misc.debounce(this._codemirrorValueChanged.bind(this)));
if (!this.codeMirror.getOption('indentWithTabs')) {
this.codeMirror.setOption('extraKeys', {
Tab: cm => {
var spaces = Array(this.codeMirror.getOption('indentUnit') + 1).join(' ');
cm.replaceSelection(spaces);
}
});
}
// Do this a bit later so we don't block the render process
setTimeout(() => {
this._codemirrorSetValue(value || '');
}, 50);
this._codemirrorSetOptions();
};
_isJSON (mode) {
if (!mode) {
return false;
}
return mode.indexOf('json') !== -1
}
_isXML (mode) {
if (!mode) {
return false;
}
return mode.indexOf('xml') !== -1
}
_handleBeautify () {
trackEvent('Request', 'Beautify');
this._prettify(this.codeMirror.getValue());
}
async _prettify (code) {
if (this._isXML(this.props.mode)) {
code = this._formatXML(code);
} else {
code = this._formatJSON(code);
}
this.codeMirror.setValue(code);
}
_formatJSON (code) {
try {
let obj = JSON.parse(code);
if (this.props.updateFilter && this.state.filter) {
obj = JSONPath({json: obj, path: this.state.filter});
}
return vkBeautify.json(obj, '\t');
} catch (e) {
// That's Ok, just leave it
return code;
}
}
_formatXML (code) {
if (this.props.updateFilter && this.state.filter) {
try {
const dom = new DOMParser().parseFromString(code);
const nodes = xpath.select(this.state.filter, dom);
const inner = nodes.map(n => n.toString()).join('\n');
code = `
Use {link} to filter the response body. Here are some examples that you might use on a book store API.
{json ? '$.store.books[*].title' : '/store/books/title'}
|
Get titles of all books in the store |
{json ? '$.store.books[?(@.price < 10)].title' : '/store/books[price < 10]'}
|
Get books costing less than $10 |
{json ? '$.store.books[-1:]' : '/store/books[last()]'}
|
Get the last book in the store |
{json ? '$.store.books.length' : 'count(/store/books)'}
|
Get the number of books in the store |