UNPKG

feeles-ide

Version:

The hackable and serializable IDE to make learning material

367 lines (336 loc) 12.2 kB
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;