feeles-ide
Version:
The hackable and serializable IDE to make learning material
367 lines (336 loc) • 12.2 kB
JavaScript
import _slicedToArray from 'babel-runtime/helpers/slicedToArray';
import _getIterator from 'babel-runtime/core-js/get-iterator';
import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _createClass from 'babel-runtime/helpers/createClass';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
import _WeakMap from 'babel-runtime/core-js/weak-map';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { JSHINT } from 'jshint';
import deepEqual from 'deep-equal';
import reduce from 'lodash/reduce';
import includes from 'lodash/includes';
import CodeMirror from 'codemirror';
import 'codemirror/mode/meta';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/css/css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/yaml/yaml';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/html-hint';
import 'codemirror/addon/hint/css-hint';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
// import 'codemirror/addon/scroll/simplescrollbars';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/keymap/sublime';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/dialog/dialog.css';
// import 'codemirror/addon/scroll/simplescrollbars.css';
import 'codemirror/addon/fold/foldgutter.css';
import glslMode from 'glsl-editor/glsl';
import './codemirror-hint-extension';
import CodeMirrorComponent from '../../utils/CodeMirrorComponent';
glslMode(CodeMirror);
CodeMirror.modeInfo.push({
name: 'glsl',
mime: 'text/x-glsl',
mode: 'glsl'
});
// YAML のエイリアス (.yml) (text/yaml)
CodeMirror.modeInfo.push({
name: 'YAML',
mimes: ['text/yaml', 'text/x-yaml'],
mode: 'yaml',
ext: ['yml', 'yaml'],
alias: ['yml']
});
// segments の参照と現在の line との関係を一旦保持するマップ
var segmentsLineMap = new _WeakMap();
var Editor = function (_PureComponent) {
_inherits(Editor, _PureComponent);
function Editor() {
var _ref;
var _temp, _this, _ret;
_classCallCheck(this, Editor);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Editor.__proto__ || _Object$getPrototypeOf(Editor)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
jshintrc: null,
dropdowns: [],
dropdownLineWidgets: []
}, _this.handleCodemirror = function (ref) {
if (!ref) return;
var cm = ref.getCodeMirror();
if (cm) {
_this.showHint(cm);
_this.props.codemirrorRef(cm);
// ドロップダウンウィジェット
cm.on('changes', _this.handleUpdateWidget);
cm.on('swapDoc', function () {
_this.clearDropdown();
_this.handleUpdateWidget(cm, []);
});
_this.handleUpdateWidget(cm, []);
}
}, _this.handleUpdateWidget = function (cm, batch) {
var origin = batch[0] && batch[0].origin; // e.g. '+input'
var dropdowns = reduce(cm.getValue('\n').split('\n'), function (prev, text, line) {
// Syntax: ('▼ スキン', _kきし)
var segments = /^(.*\(['"])(▼[^'"]*)(['"],\s*)([^)]*)\)/.exec(text);
if (segments) {
// line は独立して保持(異なる行でも移動しただけなら問題ない)
segmentsLineMap.set(segments, line);
// 新しいデータを格納
prev = prev.concat([segments]);
}
return prev;
}, []);
// 中身が変わっていたら更新
if (includes(['setValue', 'undo'], origin) || !deepEqual(dropdowns, _this.state.dropdowns)) {
// 前回の LineWidget を消去
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = _getIterator(_this.state.dropdownLineWidgets), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
item.clear(); // remove element
}
// 今回の LineWidget を追加
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var dropdownLineWidgets = dropdowns.map(function (segments) {
var line = segmentsLineMap.get(segments); // さっき保持した line
return _this.renderDropdown(cm, segments, line);
});
_this.setState({
dropdowns: dropdowns,
dropdownLineWidgets: dropdownLineWidgets
});
}
}, _this.clearDropdown = function () {
// 全ての LineWidget を消去
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = _getIterator(_this.state.dropdownLineWidgets), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
item.clear(); // remove element
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
_this.setState({
dropdowns: [],
dropdownLineWidgets: []
});
}, _this.renderDropdown = function (cm, segments, line) {
var _segments = _slicedToArray(segments, 5),
_prefix = _segments[1],
_label = _segments[2],
_right = _segments[3],
_value = _segments[4];
var label = document.createElement('span');
label.textContent = _label;
label.classList.add('Feeles-dropdown-label');
var right = document.createElement('span');
right.textContent = _right;
right.classList.add('Feeles-dropdown-blank');
var value = document.createElement('span');
value.textContent = _value;
value.classList.add('Feeles-dropdown-value');
value.addEventListener('click', _this.handleValueClick);
var button = document.createElement('span');
button.appendChild(label); // "▼ スキン"
button.appendChild(right); // "', "
button.appendChild(value); // _kきし
button.classList.add('Feeles-dropdown-button');
var allOfLeft = _prefix + _label + _right; // value より左の全て
var ch = allOfLeft.length;
var shadow = document.createElement('span');
shadow.appendChild(button);
shadow.classList.add('Feeles-dropdown-shadow');
var parent = document.createElement('div');
parent.classList.add('Feeles-widget', 'Feeles-dropdown');
parent.appendChild(shadow);
var pos = { line: line, ch: _prefix.length };
var _cm$charCoords = cm.charCoords(pos, 'local'),
left = _cm$charCoords.left;
parent.style.transform = 'translate(' + left + 'px, -1.3rem)';
// ウィジェット追加
var widget = cm.addLineWidget(line, parent);
// クリックイベント追加
button.addEventListener('click', function () {
var line = cm.doc.getLineNumber(widget.line);
// Open dropdown menu
var listName = _label.substr(1).trim();
var list = _this.state.dropdownConfig[listName];
if (list) {
var _hint = {
from: { line: line, ch: ch },
to: { line: line, ch: ch + _value.length },
list: list.map(function (item) {
return {
text: item.body,
displayText: item.body + ' ' + (item.label || '')
};
})
};
// CodeMirror.on(hint, 'pick', this.handleSaveAndRun);
cm.showHint({
completeSingle: false,
hint: function hint() {
return _hint;
}
});
cm.focus();
}
}, true);
return widget;
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(Editor, [{
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps) {
if (this.props.file === nextProps.file) {
return false;
}
return true;
}
}, {
key: 'componentWillMount',
value: function componentWillMount() {
var jshintrc = this.props.findFile('.jshintrc');
if (jshintrc) {
// .jshintrc があれば JSHint でチェック
window.JSHINT = JSHINT;
try {
this.setState({
jshintrc: JSON.parse(jshintrc.text)
});
} catch (e) {
// continue regardless of error
}
}
this.setState({
dropdownConfig: this.props.loadConfig('dropdown'),
dropdowns: [],
dropdownLineWidgets: []
});
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.props.fileView !== nextProps.fileView) {
this.setState({
dropdownConfig: this.props.loadConfig('dropdown')
});
}
}
}, {
key: 'showHint',
value: function showHint(cm) {
var _this2 = this;
if (!this.props.showHint) {
return;
}
var getFiles = this.props.getFiles;
cm.on('change', function (_cm, change) {
if (change.origin === 'setValue' || change.origin === 'complete') return;
var cursor = cm.getCursor();
cm.scrollIntoView(cursor);
cm.showHint({
completeSingle: false,
files: getFiles(),
snippets: _this2.props.snippets
});
});
}
}, {
key: 'render',
value: function render() {
var _props = this.props,
file = _props.file,
lineNumbers = _props.lineNumbers;
var meta = CodeMirror.findModeByMIME(file.type);
var mode = meta && meta.mode;
return React.createElement(CodeMirrorComponent, {
id: file.key,
value: file.text,
onDocChanged: this.props.onDocChanged,
mode: mode,
lineNumbers: lineNumbers,
keyMap: 'sublime',
foldGutter: true,
foldOptions: this.props.foldOptions,
extraKeys: this.props.extraKeys,
ref: this.handleCodemirror
});
}
}]);
return Editor;
}(PureComponent);
Editor.propTypes = {
file: PropTypes.object.isRequired,
getFiles: PropTypes.func.isRequired,
getConfig: PropTypes.func.isRequired,
codemirrorRef: PropTypes.func.isRequired,
snippets: PropTypes.array.isRequired,
showHint: PropTypes.bool.isRequired,
extraKeys: PropTypes.object.isRequired,
foldOptions: PropTypes.object,
lineNumbers: PropTypes.bool.isRequired,
findFile: PropTypes.func.isRequired,
onDocChanged: PropTypes.func.isRequired,
loadConfig: PropTypes.func.isRequired,
fileView: PropTypes.object.isRequired
};
Editor.defaultProps = {
getFiles: function getFiles() {
return [];
},
codemirrorRef: function codemirrorRef() {},
snippets: [],
showHint: true,
extraKeys: {},
lineNumbers: true,
foldOptions: {},
onDocChanged: function onDocChanged() {}
};
export default Editor;