UNPKG

feeles-ide

Version:

The hackable and serializable IDE to make learning material

692 lines (623 loc) 24 kB
import _String$raw from 'babel-runtime/core-js/string/raw'; import _taggedTemplateLiteral from 'babel-runtime/helpers/taggedTemplateLiteral'; import _Promise from 'babel-runtime/core-js/promise'; import _extends from 'babel-runtime/helpers/extends'; import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray'; import _slicedToArray from 'babel-runtime/helpers/slicedToArray'; import _getIterator from 'babel-runtime/core-js/get-iterator'; import _regeneratorRuntime from 'babel-runtime/regenerator'; import _asyncToGenerator from 'babel-runtime/helpers/asyncToGenerator'; import _Map from 'babel-runtime/core-js/map'; 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'; var _templateObject = _taggedTemplateLiteral(['(const|let|var)s', '(d+)s'], ['(const|let|var)\\s', '(\\d+)\\s']); import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import FlatButton from 'material-ui/FlatButton'; import LinearProgress from 'material-ui/LinearProgress'; import HardwareKeyboardBackspace from 'material-ui/svg-icons/hardware/keyboard-backspace'; import ContentSave from 'material-ui/svg-icons/content/save'; import { Pos } from 'codemirror'; import beautify from 'js-beautify'; import includes from 'lodash/includes'; import Editor from './Editor'; import CreditBar from './CreditBar'; import PlayMenu from './PlayMenu'; import AssetPane from './AssetPane'; import ErrorPane from './ErrorPane'; import zenkakuToHankaku from './zenkakuToHankaku'; var getStyle = function getStyle(props, state, context) { var palette = context.muiTheme.palette; return { root: { position: 'absolute', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch' }, editorContainer: { flex: '1 1 auto', position: 'relative' }, menuBar: { display: 'flex', backgroundColor: palette.canvasColor, borderBottom: '1px solid ' + palette.primary1Color, zIndex: 3 }, barButton: { padding: 0, lineHeight: 2 }, barButtonLabel: { fontSize: '.5rem' }, progressColor: palette.primary1Color, progress: { borderRadius: 0 } }; }; // file -> prevFile を参照する var prevFiles = new _WeakMap(); var SourceEditor = function (_PureComponent) { _inherits(SourceEditor, _PureComponent); function SourceEditor() { var _ref, _this2 = this; var _temp, _this, _ret; _classCallCheck(this, SourceEditor); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = SourceEditor.__proto__ || _Object$getPrototypeOf(SourceEditor)).call.apply(_ref, [this].concat(args))), _this), _this.state = { showHint: !_this.props.file.is('json'), hasHistory: false, hasChanged: false, loading: false, snippets: [], assetFileName: null, assetLineNumber: 0, assetScope: null, appendToHead: true, classNameStyles: [] }, _this._widgets = new _Map(), _this.handleSave = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var text, prevFile, file, babelrc; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: if (_this.codemirror) { _context.next = 2; break; } return _context.abrupt('return'); case 2: _this.beautify(_this.codemirror); // Auto beautify text = _this.codemirror.getValue(); if (!(text === _this.props.file.text)) { _context.next = 6; break; } return _context.abrupt('return'); case 6: _this.setState({ hasChanged: false, loading: true }); prevFile = _this.props.file; _context.next = 10; return _this.props.putFile(_this.props.file, _this.props.file.set({ text: text })); case 10: file = _context.sent; prevFiles.set(file, prevFile); // Like a watching babelrc = _this.props.getConfig('babelrc'); file.babel(babelrc, function (e) { _this.props.selectTabFromFile(file); // あらたな Babel Error が発生したときを検知して, // ダイアログを表示させる (エラーの詳細は file.error を参照する) _this.forceUpdate(); // 再描画 console.info(e); }); _this.setState({ loading: false }); case 15: case 'end': return _context.stop(); } } }, _callee, _this2); })), _this.handleUndo = function () { _this.codemirror.undo(); }, _this.handleUpdateWidget = function (cm) { _this._widgets.clear(); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = _getIterator(cm.getValue('\n').split('\n').entries()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _step$value = _slicedToArray(_step.value, 2), line = _step$value[0], text = _step$value[1]; _this.updateWidget(cm, line, text); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } }, _this.updateWidget = function (cm, line, text) { // Syntax: /*+ モンスター アイテム */ var asset = /^(.*)(\/\*)(\+[^*]+)(\*\/)/.exec(text); if (asset) { var _asset$map = asset.map(function (t) { return t.replace(/\t/g, ' '); }), _asset$map2 = _slicedToArray(_asset$map, 5), _prefix = _asset$map2[1], _left = _asset$map2[2], _label = _asset$map2[3], _right = _asset$map2[4]; var prefix = document.createElement('span'); prefix.textContent = _prefix; prefix.classList.add('Feeles-asset-blank'); var left = document.createElement('span'); left.textContent = _left; left.classList.add('Feeles-asset-blank'); var label = document.createElement('span'); label.textContent = _label; var right = document.createElement('span'); right.textContent = _right; right.classList.add('Feeles-asset-blank'); var button = document.createElement('span'); button.classList.add('Feeles-asset-button'); button.onclick = function () { _this.setState({ assetScope: _label.substr(1).trim(), assetLineNumber: line, appendToHead: false }); }; button.appendChild(left); button.appendChild(label); button.appendChild(right); var parent = document.createElement('div'); parent.classList.add('Feeles-widget', 'Feeles-asset'); parent.appendChild(prefix); parent.appendChild(button); _this._widgets.set(line, parent); } }, _this.handleValueClick = function (event) { // Put cursor into editor if (_this.codemirror) { var locate = { left: event.x, top: event.y }; var pos = _this.codemirror.coordsChar(locate); _this.codemirror.focus(); _this.codemirror.setCursor(pos); } }, _this.handleRenderWidget = function (cm) { // remove old widgets var _arr = [].concat(_toConsumableArray(document.querySelectorAll('.Feeles-asset'))); for (var _i = 0; _i < _arr.length; _i++) { var widget = _arr[_i]; if (widget.parentNode) { widget.parentNode.removeChild(widget); } } // render new widgets var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = _getIterator(_this._widgets.entries()), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _step2$value = _slicedToArray(_step2.value, 2), i = _step2$value[0], element = _step2$value[1]; cm.addWidget(new Pos(i, 0), element); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } }, _this.handleIndexReplacement = function (cm, change) { if (!includes(['asset', 'paste'], change.origin)) return; var _arr2 = ['item', 'map']; var _loop = function _loop() { var keyword = _arr2[_i2]; // すでに使われている item{N} のような変数を探す. // 戻り値は Array<Number> // e.g. From "const item1 = 'hello';", into [1] var usedIndexes = searchItemIndexes(cm.getValue('\n'), keyword); if (usedIndexes.length < 1) return 'continue'; var sourceText = change.text.join('\n'); if (usedIndexes.some(function (i) { return includes(sourceText, keyword + i); })) { // もし名前が競合していたら… var max = Math.max.apply(null, usedIndexes); var regExp = new RegExp(keyword + '(\\d+)', 'g'); var text = sourceText.replace(regExp, function (match, n) { // item{n} => item{n+max} n = n >> 0; return keyword + (n + max); }); change.update(change.from, change.to, text.split('\n')); } }; for (var _i2 = 0; _i2 < _arr2.length; _i2++) { var _ret2 = _loop(); if (_ret2 === 'continue') continue; } }, _this.handleAssetClose = function () { _this.setState({ assetFileName: null, assetScope: null }); }, _this.setLocation = function () { var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(href) { return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _context2.next = 2; return _this.handleSave(); case 2: return _context2.abrupt('return', _this.props.setLocation(href)); case 3: case 'end': return _context2.stop(); } } }, _callee2, _this2); })); return function (_x) { return _ref3.apply(this, arguments); }; }(), _this.handleSaveAndRun = function () { return _this.setLocation(); }, _this.handleAssetInsert = function (_ref4) { var code = _ref4.code; var assetLineNumber = _this.state.assetLineNumber; var pos = new Pos(assetLineNumber, 0); var end = new Pos(pos.line + code.split('\n').length, 0); code = _this.state.appendToHead ? '\n' + code : code + '\n'; _this.codemirror.replaceRange(code, pos, pos, 'asset'); // トランジション(フェードイン) var fadeInMarker = _this.codemirror.markText(pos, end, { className: 'emphasize-' + Date.now(), clearOnEnter: true }); _this.emphasizeTextMarker(fadeInMarker); // スクロール _this.codemirror.scrollIntoView({ from: pos, to: end }, 10); // カーソル (挿入直後に undo したときスクロールが上に戻るのを防ぐ) _this.codemirror.focus(); _this.codemirror.setCursor(end); // Pane をとじる _this.handleAssetClose(); // 実行 (UIが固まらないように時間をおいている) setTimeout(_this.setLocation, 1000); }, _this.emphasizeTextMarker = function () { var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(textMarker) { var transitions, begin, end; return _regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: transitions = _this.context.muiTheme.transitions; begin = { className: textMarker.className, style: 'opacity: 0; background-color: rgba(0,0,0,1)' }; end = { className: textMarker.className, style: 'opacity: 1; background-color: rgba(0,0,0,0.1); transition: ' + transitions.easeOut() }; textMarker.on('clear', function () { _this.setState(function (prevState) { return { classNameStyles: prevState.classNameStyles.filter(function (item) { return begin !== item && end !== item; }) }; }); }); _this.setState(function (prevState) { return { classNameStyles: prevState.classNameStyles.concat(begin) }; }); _context3.next = 7; return wait(500); case 7: _this.setState(function (prevState) { return { classNameStyles: prevState.classNameStyles.map(function (item) { return item === begin ? end : item; }) }; }); case 8: case 'end': return _context3.stop(); } } }, _callee3, _this2); })); return function (_x2) { return _ref5.apply(this, arguments); }; }(), _this.handleIndentLine = function (cm, change) { if (!includes(['asset', 'paste'], change.origin)) return; var from = change.from; var to = new Pos(from.line + change.text.length, 0); // インデント for (var line = from.line; line < to.line; line++) { cm.indentLine(line); } }, _this.handleRestore = function () { // 保存する前の状態に戻す var prevFile = prevFiles.get(_this.props.file); if (prevFile) { _this.setValue(prevFile.text); _this.setLocation(); } }, _this.beautify = function () { var _this$props = _this.props, file = _this$props.file, fileView = _this$props.fileView; var prevValue = _this.codemirror.getValue(); var setValueWithoutHistory = function setValueWithoutHistory(replacement) { // undo => beautify => setValue することで history を 1 つに var _this$codemirror$getS = _this.codemirror.getScrollInfo(), left = _this$codemirror$getS.left, top = _this$codemirror$getS.top; _this.codemirror.undo(); _this.codemirror.setValue(replacement); _this.codemirror.scrollTo(left, top); }; // import .jsbeautifyrc var configs = {}; try { var runCommand = fileView.getFileByFullPath('.jsbeautifyrc'); if (runCommand) { configs = JSON.parse(runCommand.text); } } catch (error) { console.info(error); } if (file.is('javascript') || file.is('json')) { setValueWithoutHistory(beautify(prevValue, configs.js || {})); } else if (file.is('html')) { setValueWithoutHistory(beautify.html(prevValue, configs.html || {})); } else if (file.is('css')) { setValueWithoutHistory(beautify.css(prevValue, configs.css || {})); } }, _temp), _possibleConstructorReturn(_this, _ret); } _createClass(SourceEditor, [{ key: 'componentWillMount', value: function componentWillMount() { this.setState({ snippets: this.props.getConfig('snippets')(this.props.file) }); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (this.props.fileView !== nextProps.fileView) { this.setState({ snippets: nextProps.getConfig('snippets')(nextProps.file) }); } } }, { key: 'componentDidMount', value: function componentDidMount() { var _this3 = this; if (this.codemirror) { this.codemirror.on('beforeChange', zenkakuToHankaku); this.codemirror.on('beforeChange', this.handleIndexReplacement); this.codemirror.on('change', this.handleIndentLine); var onChange = function onChange(cm) { _this3.setState({ hasHistory: cm.historySize().undo > 0, hasChanged: cm.getValue('\n') !== _this3.props.file.text }); }; this.codemirror.on('change', onChange); this.codemirror.on('swapDoc', onChange); this.codemirror.on('change', this.handleUpdateWidget); this.codemirror.on('swapDoc', this.handleUpdateWidget); this.codemirror.on('update', this.handleRenderWidget); this.handleUpdateWidget(this.codemirror); this.handleRenderWidget(this.codemirror); } } }, { key: 'setValue', value: function setValue(value) { var _codemirror$getScroll = this.codemirror.getScrollInfo(), left = _codemirror$getScroll.left, top = _codemirror$getScroll.top; this.codemirror.setValue(value); this.codemirror.scrollTo(left, top); } }, { key: 'render', value: function render() { var _this4 = this; var _props = this.props, file = _props.file, localization = _props.localization; var showHint = this.state.showHint; var styles = getStyle(this.props, this.state, this.context); // const snippets = this.props.getConfig('snippets')(file); var extraKeys = { 'Ctrl-Enter': function CtrlEnter() { // Key Binding された操作の直後にカーソルが先頭に戻ってしまう(?)ため, // それをやり過ごしてから実行する window.setTimeout(_this4.handleSaveAndRun, 10); }, 'Ctrl-Alt-B': function CtrlAltB() { // Key Binding された操作の直後にカーソルが先頭に戻ってしまう(?)ため, // それをやり過ごしてから実行する window.setTimeout(_this4.beautify, 10); } }; var foldOptions = { widget: ' 📦 ', minFoldSize: 1, scanUp: false }; return React.createElement( 'div', { style: styles.root }, React.createElement( 'style', null, this.state.classNameStyles.map(function (item) { return '.' + item.className + ' { ' + item.style + ' } '; }) ), React.createElement( 'div', { style: styles.menuBar }, React.createElement(FlatButton, { label: localization.editorCard.undo, disabled: !this.state.hasHistory, style: styles.barButton, labelStyle: styles.barButtonLabel, icon: React.createElement(HardwareKeyboardBackspace, null), onClick: this.handleUndo }), React.createElement(FlatButton, { label: localization.editorCard.save, disabled: !this.state.hasChanged, style: styles.barButton, labelStyle: styles.barButtonLabel, icon: React.createElement(ContentSave, null), onClick: this.handleSaveAndRun }), React.createElement('div', { style: { flex: '1 1 auto' } }), React.createElement(PlayMenu, { getFiles: this.props.getFiles, setLocation: this.setLocation, href: this.props.href, localization: this.props.localization }) ), this.state.loading ? React.createElement(LinearProgress, { color: styles.progressColor, style: styles.progress }) : null, React.createElement( 'div', { style: styles.editorContainer }, React.createElement(AssetPane, { fileView: this.props.fileView, open: !!this.state.assetScope, scope: this.state.assetScope, loadConfig: this.props.loadConfig, findFile: this.props.findFile, handleClose: this.handleAssetClose, handleAssetInsert: this.handleAssetInsert, localization: this.props.localization }), React.createElement(Editor, _extends({}, this.props, { showHint: showHint, snippets: this.state.snippets, codemirrorRef: function codemirrorRef(ref) { return _this4.codemirror = ref; }, onDocChanged: this.props.onDocChanged, extraKeys: extraKeys, foldOptions: foldOptions, loadConfig: this.props.loadConfig, fileView: this.props.fileView })) ), React.createElement(ErrorPane, { error: file.error, localization: localization, onRestore: this.handleRestore, canRestore: prevFiles.has(file) }), React.createElement(CreditBar, { file: file, openFileDialog: this.props.openFileDialog, putFile: this.props.putFile, localization: localization, getFiles: this.props.getFiles }) ); } }]); return SourceEditor; }(PureComponent); SourceEditor.propTypes = { fileView: PropTypes.object.isRequired, file: PropTypes.object.isRequired, files: PropTypes.array.isRequired, getFiles: PropTypes.func.isRequired, setLocation: PropTypes.func.isRequired, href: PropTypes.string.isRequired, getConfig: PropTypes.func.isRequired, loadConfig: PropTypes.func.isRequired, findFile: PropTypes.func.isRequired, reboot: PropTypes.bool.isRequired, localization: PropTypes.object.isRequired, openFileDialog: PropTypes.func.isRequired, putFile: PropTypes.func.isRequired, closeSelectedTab: PropTypes.func.isRequired, selectTabFromFile: PropTypes.func.isRequired, onDocChanged: PropTypes.func.isRequired }; SourceEditor.contextTypes = { muiTheme: PropTypes.object.isRequired }; export default SourceEditor; function wait(millisec) { return new _Promise(function (resolve) { setTimeout(resolve, millisec); }); } function searchItemIndexes(text, keyword) { var limit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000; var regExp = new RegExp(_String$raw(_templateObject, keyword), 'g'); text = beautify(text); var indexes = []; for (var i = 0, result = null; (result = regExp.exec(text)) && i < limit; i++) { indexes.push(+result[2]); } return indexes; }